/* Typography (visual polish pass, roadmap items 49-51): Barlow replaces the old bare
   system-font stack for body/UI text everywhere -- Anton stays reserved for the wordmark and
   major (theme-driven, via --rb-heading-font) page headings only, never body text, matching the
   convention already established on the mobile app. IBM Plex Mono is used for small meta text
   (timestamps, category/type labels, post metadata) -- see the rule near the bottom of this file.
   All three load from a single Google Fonts <link> in base.html. This is a sitewide change, so it
   also reaches the anonymous pre-login view -- deliberate: fonts are explicitly outside the theme
   system's protected "pixel-identical colors" concern (see CLAUDE.md's "App shell & typography"
   section for the full reasoning), unlike --rb-bg/--rb-text/etc., which stay untouched for an
   anonymous request. */
:root {
    --rb-body-font: 'Barlow', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    --rb-mono-font: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    /* App shell chrome (roadmap items 49/50): a subtle tint of --rb-bg toward --rb-text, not a
       hardcoded color -- the same color-mix()-toward-text-color trick already used below by
       .composer textarea's background etc. Mixing toward --rb-text (rather than a fixed
       black/white) is direction-agnostic: --rb-text is always chosen to contrast against --rb-bg,
       so this always shifts the shell a little *toward* that contrast regardless of whether the
       active theme is light or dark -- the CSS equivalent of the mobile app's
       deriveSurfaceVariant() (RideBuddy/.../ui/theme/Theme.kt), which does the same lerp-toward-
       black-or-white-by-luminance trick in Kotlin, just computed a different way. See CLAUDE.md's
       "App shell & typography" section for the full reasoning. */
    --rb-shell-bg: color-mix(in srgb, var(--rb-bg, #15141a) 90%, var(--rb-text, #edeae3) 10%);
    --rb-shell-border: color-mix(in srgb, var(--rb-text, #edeae3) 14%, transparent);
}

body {
    margin: 0;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--rb-bg, #15141a);
    color: var(--rb-text, #edeae3);
    font-family: var(--rb-body-font);
}

main {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
}

h1 {
    font-size: 2.5rem;
    margin-bottom: 0.5rem;
    font-family: var(--rb-heading-font, inherit);
    text-wrap: balance;
}

h2 {
    font-size: 1.2rem;
    font-weight: 700;
    margin: 0 0 0.25rem;
    text-wrap: balance;
}

@media (max-width: 480px) {
    h1 {
        font-size: 2rem;
    }
}

p {
    color: var(--rb-label, #a39fae);
}

.site-nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 1.5rem;
}

.site-nav a, .site-nav button {
    color: var(--rb-accent, #edeae3);
    background: none;
    border: none;
    font: inherit;
    text-decoration: none;
    cursor: pointer;
}

/* Wordmark stays Anton regardless of the rider's theme font choice -- brand identity, not a
   theme-driven heading (see the typography note above :root). Higher specificity than the plain
   `.site-nav a` rule above (a.site-nav__brand beats a bare class), so this only overrides
   font-family/letter-spacing without touching that rule's color/cursor/etc. */
.site-nav a.site-nav__brand {
    font-family: 'Anton', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    letter-spacing: 0.02em;
}

.error {
    color: var(--rb-accent, #ff6b6b);
}

/* Public page shell (generalized 2026-09-03, originally the "/" -only landing hero) -- now shared
   by every anonymous page (login/register/forgot-password/about/contact/status/beta-download/
   index), not just "/": see base.html's anonymous branch. Deliberately NOT themed via --rb-*
   tokens the way logged-in pages are: this is the brand identity every anonymous visitor sees the
   same way, independent of any rider's stored theme (which an anonymous request never resolves
   anyway -- see base.html's "pixel-identical" note this replaced). Overrides the bare `main`
   rule's center-everything default with its own full-page flex column: header raised at the top,
   content flexing to fill whatever's left (so a short page like "/" still fills the viewport and
   pins the footer to the bottom, while a tall page like Status/beta-download just grows normally).
*/
.public-shell {
    width: 100%;
    min-height: 100vh;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    text-align: center;
    /* Two background layers: a dark gradient painted OVER the photo -- this is what keeps the
       chrome logo/nav/page content legible regardless of how bright/busy the photo is at any given
       crop, and is also what makes the logo's mix-blend-mode below work reliably (a bright patch of
       sky behind the logo would fight the blend; the overlay guarantees there isn't one).
       background-attachment:fixed sizes/positions the photo relative to the *viewport*, not this
       element's own box -- deliberate, since this element's height now varies hugely page to page
       (roughly one viewport on "/", much taller on Status/beta-download); without `fixed`,
       background-size:cover would rescale the photo to cover whatever the tallest page's full
       height is, zooming it in far more than intended on short pages. Content scrolls over a
       pinned backdrop instead, the standard fix for a shared hero-style background across
       differently-sized pages. */
    background:
        linear-gradient(rgba(10, 9, 12, 0.68), rgba(10, 9, 12, 0.78)),
        url("../images/landing-background.70fd68cac40e.jpg") center 35% / cover no-repeat;
    background-attachment: fixed;
}

/* Raised near the top (direct product request -- the original cut vertically centered this in the
   viewport) rather than centered or flexed to fill space. */
.public-shell__header {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.25rem;
    padding: 2rem 1.5rem 1.25rem;
}

.public-shell__logo-link {
    display: inline-flex;
}

/* Logo refresh (2026-09-09): shiftntwist-logo.png is now a genuinely alpha-transparent asset
   (a real alpha channel from a higher-quality source, not a flat RGB render on solid black) --
   mix-blend-mode:screen is gone, since it was only ever a workaround for the old asset having no
   real transparency of its own. The new file also carries its own baked-in "pop" treatment (a
   thin dark keyline right at the mark's edge plus a soft ambient shadow) so it reads clearly
   against the busy photo backdrop without needing a guaranteed-dark background the way the old
   blend-mode trick did -- safe to use over any background now, not just this shell's own dark
   overlay. Sized smaller than the original single-page hero cut (720px) since this repeats as a
   persistent header on every page, not a one-time splash moment. */
.public-shell__logo {
    width: min(420px, 62vw);
    height: auto;
}

.public-shell__nav {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 0.5rem 2rem;
    width: 100%;
    max-width: 640px;
}

.public-shell__nav a {
    color: #f2efe9;
    text-decoration: none;
    font-size: 0.95rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 0.4rem 0.1rem;
    border-bottom: 2px solid transparent;
    transition: color 0.15s ease, border-color 0.15s ease;
}

.public-shell__nav a:hover,
.public-shell__nav a:focus-visible {
    color: #ff5a4d;
    border-bottom-color: #ff5a4d;
}

/* Fills whatever vertical space the header/footer don't use -- what keeps a short page (index)
   filling the viewport while a tall one (Status, beta-download) just grows past it normally. Each
   page's own <main class="..."> (login-page/static-page/etc.) renders inside this and keeps its
   own max-width/padding from the shared page-container rule above. */
.public-shell__content {
    flex: 1;
    width: 100%;
}

.public-shell__footer {
    padding: 1.25rem 1.5rem;
    color: color-mix(in srgb, #f2efe9 65%, transparent);
    font-size: 0.8rem;
}

.public-shell__footer p {
    margin: 0;
    color: inherit;
}

/* Disclaimer sits directly above the copyright line, its own paragraph with a small bottom
   margin to visually separate the two -- slightly more opaque than the copyright text since it's
   the more load-bearing of the two lines, centered and capped in width so it wraps to a readable
   line length instead of stretching edge-to-edge on wide viewports. Qualified as
   `.public-shell__footer p.public-shell__footer-disclaimer` (not just the bare class) so its own
   margin actually beats `.public-shell__footer p` above on specificity, rather than reaching for
   !important. */
.public-shell__footer p.public-shell__footer-disclaimer {
    max-width: 42rem;
    margin: 0 auto 0.6rem;
    color: color-mix(in srgb, #f2efe9 82%, transparent);
}

.public-shell__footer-disclaimer strong {
    color: #f2efe9;
}

.public-shell__footer-links {
    margin-bottom: 0.6rem;
}

.public-shell__footer a {
    color: inherit;
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, #f2efe9 40%, transparent);
}

.public-shell__footer a:hover,
.public-shell__footer a:focus-visible {
    color: #ff5a4d;
    text-decoration-color: currentColor;
}

/* Homepage hero + three-goal section (replaced the original single-line tagline 2026-09-10, direct
   product request for a professional, motorcycle-focused description of the platform's goals
   instead of a bare one-liner). Unlike the old tagline (short enough to vertically center in
   .public-shell__content), this content is taller than one viewport on most screens, so it just
   flows normally within that flex-1 container rather than centering. */
.landing-hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3rem;
    padding: 2rem 1.5rem 4rem;
    text-align: center;
}

.landing-hero__intro {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    max-width: 42rem;
}

.landing-hero__headline {
    font-family: 'Anton', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    font-size: clamp(1.75rem, 4.5vw, 3rem);
    letter-spacing: 0.01em;
    color: #f2efe9;
    text-wrap: balance;
    margin: 0;
}

.landing-hero__subtext {
    font-size: 1.05rem;
    line-height: 1.6;
    color: color-mix(in srgb, #f2efe9 82%, transparent);
    margin: 0;
}

.landing-goals {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1.5rem;
    width: 100%;
    max-width: 64rem;
}

.landing-goals__item {
    background: color-mix(in srgb, #050506 55%, transparent);
    border: 1px solid color-mix(in srgb, #f2efe9 16%, transparent);
    border-radius: 12px;
    padding: 1.75rem 1.5rem;
    text-align: left;
}

.landing-goals__item h2 {
    font-family: 'Anton', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    font-size: 1.15rem;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: #ff5a4d;
    margin: 0 0 0.6rem;
}

.landing-goals__item p {
    font-size: 0.95rem;
    line-height: 1.6;
    color: color-mix(in srgb, #f2efe9 85%, transparent);
    margin: 0;
}

@media (max-width: 860px) {
    .landing-goals {
        grid-template-columns: 1fr;
    }
}

/* Below ~500px, nav items at the normal gap wrap unevenly (e.g. 3 on one line, a lone item on the
   next) -- a plain vertical stack keeps every item evenly centered regardless of the item count,
   rather than a fixed 2-column grid tuned for one specific count (a 2-column grid was tried when
   this nav had exactly 4 items, but its leftover-row cell isn't centered across the row for an odd
   count -- see git history -- so a count-agnostic stack replaced it once a 5th item, "Blog", was
   added 2026-09-04). */
@media (max-width: 500px) {
    .public-shell__nav {
        flex-direction: column;
        gap: 1.1rem;
    }
}

/* Visible, accent-colored focus states sitewide (visual polish pass) -- browsers' default focus
   ring doesn't track a rider's theme and can be hard to see against some custom backgrounds. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
[tabindex]:focus-visible {
    outline: 2px solid var(--rb-accent, #edeae3);
    outline-offset: 2px;
}

/* Shared hover state for the "outline button" pattern used across the composer, chat, forum, and
   settings forms (border + accent text on transparent background) -- a light accent-tinted fill on
   hover, matching the reaction/filter-chip/thread-list hover treatments already in this file. */
.composer button[type="submit"]:hover:not(:disabled),
.chat-composer button:hover:not(:disabled),
.report-form__row button[type="submit"]:hover:not(:disabled),
.forum-toolbar button:hover:not(:disabled),
.new-thread-form__row button[type="submit"]:hover:not(:disabled),
.settings-save-row button:hover:not(:disabled),
.load-earlier-btn:hover:not(:disabled),
.delete-btn:hover,
.accept-answer-btn:hover:not(:disabled) {
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 14%, transparent);
}

/* Timeline: shared layout for the feed page and the preferences page. Both are <main> elements,
   so they must explicitly override the bare `main` rule above (which centers/shrinks its
   content) -- `.timeline-page`/`.timeline-preferences-page` win on specificity since they're
   class selectors on the same element. Messaging's `.messages-page`/`.chat-page` (below) need the
   exact same override for the exact same reason, so they share this rule. */
.timeline-page,
.timeline-preferences-page,
.messages-page,
.chat-page,
.profile-page,
.rider-profile-page,
.forum-page,
.forum-thread-page,
.forum-tags-page,
.friends-page,
.groups-page,
.groups-search-page,
.group-detail-page,
.search-page,
.moderation-page,
.moderation-riders-page,
.moderation-rider-detail-page,
.current-activity-page,
.beta-download-page,
.us-statistics-page,
.feature-requests-page,
.static-page,
.weather-page,
.login-page {
    width: 100%;
    max-width: 640px;
    margin: 0 auto;
    padding: 0 1.5rem 3rem;
    /* box-sizing:border-box (added alongside the app shell, roadmap items 49-51): without it,
       width:100% + this rule's own horizontal padding add up to more than 100% of the available
       width under the default content-box model, causing real horizontal page overflow at narrow
       viewports -- caught live while verifying the new mobile drawer layout (a pre-existing gap in
       this rule, not something the shell introduced, but now visibly relevant since the shell's
       mobile breakpoint is the first place this file specifically calls out testing at). */
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    text-align: left;
}

.timeline-page {
    gap: 1.5rem;
}

/* Rider activity map (ad-hoc request, 2026-09-01) -- the shared page-container rule above caps
   width at 640px, far too narrow for a 3840x2160 snapshot; overridden wider here, same pattern
   the Forum Q&A redesign already established for its own two-column layout. The image itself
   just needs to scale down to fit -- it's never upscaled past its native 3840px width. */
.current-activity-page {
    max-width: 1600px;
    gap: 1rem;
}

#current-activity-image {
    width: 100%;
    max-width: 3840px;
    height: auto;
    border: 1px solid color-mix(in srgb, var(--rb-text, #1c1b1f) 15%, transparent);
    border-radius: 8px;
}

/* Beta download page (ad-hoc request, 2026-09-02) -- reuses .settings-section/.btn-accent/
   .muted-note wholesale; only the button's own spacing and the numbered install-steps list need
   anything new. */
.beta-download-page__button {
    display: inline-block;
    margin-top: 0.75rem;
}

.beta-download-page__steps {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding-left: 1.5rem;
}

.beta-download-page__steps li {
    line-height: 1.5;
}

/* Timeline: composer */
.composer {
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    border-radius: 12px;
    padding: 1rem;
}

.composer form {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.composer textarea {
    width: 100%;
    box-sizing: border-box;
    resize: vertical;
    min-height: 4.5rem;
    padding: 0.6rem;
    border-radius: 8px;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    color: var(--rb-text, #edeae3);
    font: inherit;
}

.composer__meta {
    display: flex;
    justify-content: flex-end;
}

.composer__char-count {
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.composer__row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    align-items: flex-end;
}

.composer__field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.composer__field select,
.composer__field input[type="file"] {
    font: inherit;
    color: var(--rb-text, #edeae3);
    background: transparent;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    padding: 0.35rem 0.5rem;
}

.composer button[type="submit"] {
    align-self: flex-end;
    background: transparent;
    color: var(--rb-accent, #edeae3);
    border: 1px solid var(--rb-accent, #edeae3);
    border-radius: 8px;
    padding: 0.5rem 1.25rem;
    font-weight: 600;
    cursor: pointer;
}

.composer button[type="submit"]:disabled {
    opacity: 0.6;
    cursor: default;
}

/* Timeline: feed */
.feed-section {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.feed-section__link {
    font-size: 0.85rem;
}

/* "Since last visit" default filter (added 2026-09-06, roadmap item 8) -- a toolbar control, not
   a settings-form field, so this reuses .forum-toolbar__field's exact select styling rather than
   the .settings-field select style. */
.feed-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
}

.feed-toolbar__field {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.feed-toolbar__field select {
    font: inherit;
    color: var(--rb-text, #edeae3);
    background: transparent;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    padding: 0.35rem 0.5rem;
}

.feed {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.feed-sentinel {
    height: 1px;
}

.feed-skeleton {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.feed-skeleton__card {
    height: 5.5rem;
    border-radius: 12px;
    background: linear-gradient(
        90deg,
        color-mix(in srgb, var(--rb-text, #edeae3) 6%, transparent) 25%,
        color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent) 37%,
        color-mix(in srgb, var(--rb-text, #edeae3) 6%, transparent) 63%
    );
    background-size: 400% 100%;
    animation: feed-skeleton-shimmer 1.4s ease infinite;
}

@keyframes feed-skeleton-shimmer {
    0% { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}

.post-card {
    position: relative;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    border-radius: 12px;
    padding: 1rem;
}

/* Crash alert (ad-hoc request, added 2026-09-04) -- "bold lettering with a tinted red
   background." The red here is a fixed literal, not var(--rb-accent) -- same "this specific
   alarming meaning must never be washed out by a rider's arbitrary theme accent" reasoning
   .stat-card__value--danger (the Status page's own accident stat) already established -- mixed
   with --rb-bg only so the tint's lightness still tracks a rider's own theme rather than sitting
   as a flat overlay that could look wrong against a very light or very dark custom background. */
.post-card--crash-alert {
    background: color-mix(in srgb, red 14%, var(--rb-bg, #15141a));
    border-color: color-mix(in srgb, red 55%, var(--rb-bg, #15141a));
}

.post-card--crash-alert .post-card__content {
    font-weight: 700;
}

.post-card__header {
    display: flex;
    justify-content: space-between;
    gap: 0.5rem;
    padding-right: 1.75rem;
    font-size: 0.85rem;
    margin-bottom: 0.5rem;
}

.post-card__author {
    font-weight: 600;
    color: var(--rb-text, #edeae3);
}

.post-card__meta {
    color: var(--rb-label, #a39fae);
}

.post-card__content {
    margin: 0 0 0.75rem;
    color: var(--rb-text, #edeae3);
    white-space: pre-wrap;
    word-break: break-word;
}

.post-card__image {
    display: block;
    max-width: 100%;
    max-height: 420px;
    width: auto;
    height: auto;
    border-radius: 8px;
    margin-bottom: 0.75rem;
    object-fit: contain;
    cursor: pointer;
}

/* Crash alert map preview -- a real, live, self-hosted MapLibre GL JS embed (2026-09-07,
   replacing the earlier Google Maps iframe for map-provider-licensing reasons -- see
   timeline.js's buildCrashAlertMapPreview/initCrashAlertMap) plus an explicit "open in a new
   tab" link below it. */
.post-card__map-preview {
    margin-bottom: 0.75rem;
}

.post-card__map-preview-embed {
    display: block;
    position: relative;
    width: 100%;
    height: 260px;
    border-radius: 8px;
    overflow: hidden;
    /* A quiet placeholder fill while the map is lazy-initializing (IntersectionObserver) or its
       style/tiles are still loading -- color-mix()'d off the theme tokens like everything else in
       this file, so it never clashes with a rider's own theme. */
    background: color-mix(in srgb, var(--rb-text) 8%, transparent);
}

/* Standalone "view this location" map page (added 2026-09-07, pages/map_view.html) -- the
   destination every outbound SMS location link now points at instead of Google Maps. Taller than
   the Timeline card's own small preview embed above since this page has nothing else competing
   for vertical space. */
.map-view-page__embed {
    display: block;
    position: relative;
    width: 100%;
    height: 480px;
    border-radius: 8px;
    overflow: hidden;
    background: color-mix(in srgb, var(--rb-text) 8%, transparent);
}

.map-view-page__link {
    display: inline-block;
    margin-top: 0.4rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--rb-accent, #edeae3);
}

/* Attribution text reduced ~33% from MapLibre's own default (12px font / 20px line-height) per
   direct request -- the credit itself (required by OSM's ODbL license, never removed/hidden) can
   be made smaller/subtler, just not omitted. Scoped to every self-hosted map embed on this site
   (the Timeline crash-alert card's preview, and the standalone map-view page), not a blanket
   global MapLibre override, so a future unrelated map usage makes its own explicit sizing choice
   rather than silently inheriting this one. */
.post-card__map-preview-embed .maplibregl-ctrl-attrib,
.map-view-page__embed .maplibregl-ctrl-attrib {
    font-size: 8px;
    line-height: 13px;
}

.post-card__map-preview-link {
    display: inline-block;
    margin-top: 0.4rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--rb-accent, #edeae3);
}

/* Roadmap item 59: tapping/clicking a post's inline image opens it full-size. timeline.js's
   openLightbox() builds this overlay fresh on open and removes it from the DOM entirely on close
   (click on the backdrop/close button, or Escape) -- deliberately not toggled via [hidden] or a
   persistent hidden element, sidestepping the [hidden]-vs-class-`display` specificity gotcha
   documented elsewhere in this file (see the .feed/.feed-skeleton/.post-card__actions-menu
   comments below) by never needing to fight the cascade over it. Same --rb-* / color-mix() token
   approach as the rest of this file -- no hardcoded colors, so this still reflects a logged-in
   rider's own theme (Timeline is @login_required, so this only ever renders for one anyway). */
.lightbox-overlay {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem;
    box-sizing: border-box;
    background: color-mix(in srgb, var(--rb-bg, #15141a) 82%, black 18%);
}

.lightbox-overlay__image {
    display: block;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    border-radius: 8px;
    box-shadow: 0 8px 32px color-mix(in srgb, black 45%, transparent);
    cursor: default;
}

.lightbox-overlay__close {
    position: fixed;
    top: 1.25rem;
    right: 1.5rem;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 30%, transparent);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 80%, var(--rb-text, #edeae3) 20%);
    color: var(--rb-text, #edeae3);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
}

.lightbox-overlay__close:hover {
    background: color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
}

.post-card__reactions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

/* Same class-selector-vs-[hidden]-specificity gotcha documented repeatedly elsewhere in this file
   (.feed/.feed-skeleton/.vote-widget/etc.) -- needed now that renderReactionButtons() (timeline.js)
   fully hides this container, rather than just disabling it, for a rider's own post. */
.post-card__reactions[hidden] {
    display: none;
}

.reaction-btn {
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 999px;
    background: transparent;
    color: var(--rb-text, #edeae3);
    padding: 0.2rem 0.6rem;
    font-size: 0.95rem;
    cursor: pointer;
}

.reaction-btn--active {
    border-color: var(--rb-accent, #edeae3);
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 18%, transparent);
}

/* Timeline: per-post Delete/Report action control -- absolutely positioned within .post-card
   (position: relative above) so it sits inset top-right without disturbing the header's existing
   author/meta space-between layout; .post-card__header's padding-right above keeps a long meta
   string (e.g. "3h ago · Group Name") from running underneath it. */
.post-card__actions {
    position: absolute;
    top: 1rem;
    right: 1rem;
}

.post-card__actions-toggle {
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 999px;
    background: transparent;
    color: var(--rb-label, #a39fae);
    width: 1.75rem;
    height: 1.75rem;
    line-height: 1;
    font-size: 1rem;
    cursor: pointer;
}

.post-card__actions-menu {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 0.25rem;
    display: flex;
    flex-direction: column;
    min-width: 7rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 8px;
    background: color-mix(in srgb, var(--rb-bg, #15141a) 96%, var(--rb-text, #edeae3) 4%);
    box-shadow: 0 4px 12px color-mix(in srgb, black 25%, transparent);
    overflow: hidden;
    z-index: 5;
}

.post-card__actions-item {
    border: none;
    background: transparent;
    color: var(--rb-text, #edeae3);
    text-align: left;
    padding: 0.5rem 0.75rem;
    font: inherit;
    font-size: 0.85rem;
    cursor: pointer;
}

.post-card__actions-item:hover {
    background: color-mix(in srgb, var(--rb-text, #edeae3) 10%, transparent);
}

.post-card__actions-reported {
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.post-card__actions-menu[hidden] {
    display: none;
}

/* Timeline: report form, opened in place of the "..." control by the Report menu item. */
.report-form {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 0.25rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    width: 14rem;
    max-width: calc(100vw - 3rem);
    padding: 0.75rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 8px;
    background: color-mix(in srgb, var(--rb-bg, #15141a) 96%, var(--rb-text, #edeae3) 4%);
    box-shadow: 0 4px 12px color-mix(in srgb, black 25%, transparent);
    z-index: 5;
}

.report-form__field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.report-form__field select,
.report-form__field textarea {
    font: inherit;
    color: var(--rb-text, #edeae3);
    background: transparent;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    padding: 0.35rem 0.5rem;
    resize: vertical;
}

.report-form__row {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
}

.report-form__row button {
    font: inherit;
    font-size: 0.8rem;
    border-radius: 6px;
    padding: 0.35rem 0.6rem;
    cursor: pointer;
}

.report-form__row button[type="submit"] {
    background: transparent;
    color: var(--rb-accent, #edeae3);
    border: 1px solid var(--rb-accent, #edeae3);
    font-weight: 600;
}

.report-form__row button[type="submit"]:disabled {
    opacity: 0.6;
    cursor: default;
}

.report-form__cancel {
    background: transparent;
    color: var(--rb-label, #a39fae);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
}

.report-form__error {
    margin: 0;
    font-size: 0.8rem;
}

/* Timeline comments fast-follow (2026-09-01) -- a comment-count affordance on every post card
   ("N comments" / "Add a comment") that expands a comment thread inline within the same card (see
   CLAUDE.md's Timeline section for why inline, not a separate page). Reuses existing classes
   wherever the shape already matches rather than duplicating: comment reactions are
   .reaction-btn/.reaction-btn--active (identical pill styling to post reactions, via
   timeline.js's shared renderReactionButtons() helper), the own-comment Delete affordance reuses
   .delete-btn.delete-btn--small, "Load more comments" reuses .load-earlier-btn, and the composer's
   submit row reuses .report-form__row (including its button[type="submit"] accent-outline
   style/hover/disabled states) -- only the pieces with no existing equivalent get new rules below. */
.post-card__comments-toggle {
    align-self: flex-start;
    background: transparent;
    border: none;
    padding: 0;
    margin-top: 0.5rem;
    color: var(--rb-label, #a39fae);
    font-size: 0.85rem;
    cursor: pointer;
}

.post-card__comments-toggle:hover {
    color: var(--rb-accent, #edeae3);
}

.post-card__comments {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
}

.comment-list {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.comment-card {
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 10%, transparent);
    border-radius: 8px;
    padding: 0.6rem 0.75rem;
}

.comment-card__header {
    display: flex;
    justify-content: space-between;
    gap: 0.5rem;
    font-size: 0.8rem;
    margin-bottom: 0.3rem;
}

.comment-card__author {
    font-weight: 600;
    color: var(--rb-text, #edeae3);
}

.comment-card__meta {
    color: var(--rb-label, #a39fae);
}

.comment-card__content {
    margin: 0 0 0.5rem;
    color: var(--rb-text, #edeae3);
    font-size: 0.9rem;
    white-space: pre-wrap;
    word-break: break-word;
}

.comment-card__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
}

.comment-card__reactions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
}

/* Same [hidden]-vs-class-display gotcha as .post-card__reactions above -- fully hidden, not just
   disabled, for a rider's own comment. */
.comment-card__reactions[hidden] {
    display: none;
}

.comment-composer {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.comment-composer__field {
    font: inherit;
    color: var(--rb-text, #edeae3);
    background: transparent;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    padding: 0.4rem 0.6rem;
    resize: vertical;
}

.comment-composer__error {
    margin: 0;
    font-size: 0.8rem;
}

/* timeline.js toggles these elements with the plain `hidden` attribute (feed-skeleton,
   feed, composer-group-field before its first "audience=GROUP" selection). The UA stylesheet's
   own `[hidden] { display: none }` rule has the same specificity as a single class selector, so
   without this, whichever rule happens to come later in the cascade wins -- `.feed-skeleton`/
   `.feed`/`.composer__field`'s own `display: flex` above was silently beating it and leaving
   these visible. `[hidden]` here has higher specificity (class + attribute) than any of those, so
   it always wins regardless of source order. Caught live: the Group field rendered even with
   Audience set to Public, and the skeleton shimmer never disappeared once the feed loaded.
   `.post-card__comments[hidden]` (comments fast-follow, 2026-09-01) needs the identical override
   for the identical reason -- its own `display: flex` above would otherwise beat `[hidden]`. */
.feed-skeleton[hidden],
.feed[hidden],
.composer__field[hidden],
.post-card__comments[hidden] {
    display: none;
}

/* Timeline: mute/filter preferences */
.back-link {
    display: inline-block;
    margin-bottom: 1.5rem;
}

.mute-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.mute-list__item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    border-radius: 8px;
    padding: 0.5rem 0.75rem;
}

.mute-list__item button {
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    background: transparent;
    color: var(--rb-text, #edeae3);
    border-radius: 6px;
    padding: 0.25rem 0.6rem;
    cursor: pointer;
}

.muted-note {
    font-size: 0.85rem;
    color: var(--rb-label, #a39fae);
}

.filter-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0.75rem 0;
}

.filter-chip {
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    background: transparent;
    color: var(--rb-text, #edeae3);
    border-radius: 999px;
    padding: 0.3rem 0.8rem;
    cursor: pointer;
}

.filter-chip--active {
    border-color: var(--rb-accent, #edeae3);
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 18%, transparent);
}

/* Messaging: thread list + start-new-conversation list (pages/templates/pages/messages.html) --
   same list-item/border/`color-mix()` token approach as .mute-list above. */
.messages-page {
    gap: 1.5rem;
}

.thread-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.thread-list__item a {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    border-radius: 8px;
    padding: 0.6rem 0.85rem;
    color: var(--rb-text, #edeae3);
    text-decoration: none;
}

.thread-list__item a:hover {
    background: color-mix(in srgb, var(--rb-text, #edeae3) 6%, transparent);
}

.thread-list__name {
    font-weight: 600;
}

.thread-list__meta {
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

/* Messaging: one conversation (pages/templates/pages/chat_thread.html). */
.chat-page {
    gap: 1rem;
}

.chat-section {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.load-earlier-btn {
    align-self: center;
    background: transparent;
    color: var(--rb-accent, #edeae3);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 999px;
    padding: 0.35rem 0.9rem;
    font-size: 0.85rem;
    cursor: pointer;
    display: inline-block;
}

.load-earlier-btn:disabled {
    opacity: 0.6;
    cursor: default;
}

/* Sets its own `display` at the same class-selector specificity as [hidden] -- same reasoning
   documented above for .feed/.feed-skeleton/.composer__field/.chat-messages. */
.load-earlier-btn[hidden] {
    display: none;
}

.chat-messages {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

/* #chat-skeleton reuses .feed-skeleton (see below), whose own [hidden] override already applies;
   .chat-messages needs its own since it sets display: flex at the same class-selector
   specificity as [hidden], same reasoning as .feed/.feed-skeleton documented above. */
.chat-messages[hidden] {
    display: none;
}

/* Date dividers (added 2026-09-06) -- one per calendar day, matching the pill-shaped small-label
   treatment .state-badge/.forum-category-badge already use elsewhere in this file rather than a
   plain horizontal rule, since a thread can already run for months and a rule alone carries no
   information about *which* day changed. */
.chat-day-divider {
    align-self: center;
    margin: 0.4rem 0;
}

.chat-day-divider__label {
    display: inline-block;
    font-size: 0.75rem;
    font-family: var(--rb-mono-font, monospace);
    color: var(--rb-label, #a39fae);
    background: color-mix(in srgb, var(--rb-text, #edeae3) 8%, transparent);
    border-radius: 999px;
    padding: 0.2rem 0.75rem;
}

.chat-bubble {
    align-self: flex-start;
    max-width: 75%;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    border-radius: 12px;
    padding: 0.5rem 0.75rem;
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}

.chat-bubble--own {
    align-self: flex-end;
    border-color: var(--rb-accent, #edeae3);
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 14%, transparent);
}

.chat-bubble__text {
    margin: 0;
    color: var(--rb-text, #edeae3);
    white-space: pre-wrap;
    word-break: break-word;
}

.chat-bubble__text--error {
    font-style: italic;
    color: var(--rb-label, #a39fae);
}

/* Roadmap item 66 -- message reactions. The reaction row itself carries BOTH .chat-bubble__reactions
   AND .post-card__reactions (see messaging.js's renderMessages), and each button carries
   .reaction-btn/.reaction-btn--active directly -- deliberately reusing Timeline's own reaction pill
   treatment verbatim (flex-wrap row, pill borders, the active-state highlight) rather than
   inventing a second visual language for what's visually the same control, just without a count
   badge (a DM reaction is a single slot, not a many-riders-aggregated count). No rules of its own
   here -- .chat-bubble__reactions is a namespaced no-op hook only, matching .chat-bubble__text/
   .chat-bubble__time's own per-feature-class convention, so a future messaging-only tweak has
   somewhere to land without touching Timeline's shared .post-card__reactions styling. */

.chat-bubble__time {
    align-self: flex-end;
    font-size: 0.7rem;
    color: var(--rb-label, #a39fae);
}

.chat-composer {
    display: flex;
    gap: 0.6rem;
    align-items: flex-end;
}

.chat-composer textarea {
    flex: 1;
    box-sizing: border-box;
    resize: vertical;
    min-height: 3rem;
    padding: 0.6rem;
    border-radius: 8px;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    color: var(--rb-text, #edeae3);
    font: inherit;
}

.chat-composer button {
    background: transparent;
    color: var(--rb-accent, #edeae3);
    border: 1px solid var(--rb-accent, #edeae3);
    border-radius: 8px;
    padding: 0.5rem 1.25rem;
    font-weight: 600;
    cursor: pointer;
}

.chat-composer button:disabled {
    opacity: 0.6;
    cursor: default;
}

/* forum_thread.html reuses .chat-composer for its reply form and toggles it with the plain
   `hidden` attribute while the thread is still loading (chat_thread.html's own #chat-composer is
   never hidden, so this override wasn't needed until now) -- same class-selector-vs-[hidden]
   specificity fix as .feed/.feed-skeleton/.composer__field/.load-earlier-btn/.chat-messages above. */
.chat-composer[hidden] {
    display: none;
}

/* Profile stats (roadmap items 16/19): lifetime mileage + states-visited badges. Read-only, so no
   form/composer rules needed here -- just the skeleton (reuses .feed-skeleton), an error message
   (reuses .error), and a small stat-card grid. */
.profile-page {
    gap: 1.5rem;
}

/* Profile page tab bar ("My Timeline" / "Settings", added 2026-09-08, direct product request) --
   no tab-bar precedent existed anywhere in this codebase before this (confirmed) -- mirrors
   .forum-tabs/.forum-tab/.forum-tab--active's exact visual language (a border-bottom row of pill
   buttons) rather than inventing a new one, per CLAUDE.md's own instruction to match Forum's
   sort-tab bar. Separate rule names from .forum-tab* rather than a shared class, since the two
   bars serve different semantics (sort selection vs. content-panel switching) even though they
   look identical -- same reasoning this file's own per-file JS helper duplication already
   establishes for small, intentionally-repeated pieces. */
.profile-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    border-bottom: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    padding-bottom: 0.5rem;
}

.profile-tab {
    background: transparent;
    border: none;
    border-radius: 999px;
    padding: 0.4rem 0.9rem;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--rb-label, #a39fae);
    cursor: pointer;
}

.profile-tab:hover {
    background: color-mix(in srgb, var(--rb-text, #edeae3) 8%, transparent);
}

.profile-tab--active {
    color: var(--rb-accent, #edeae3);
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 16%, transparent);
}

.profile-tab-panel {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

/* Same class-selector-vs-[hidden]-specificity fix documented repeatedly elsewhere in this file
   (.feed/.feed-skeleton/.settings-form/.settings-field-row/etc.) -- .profile-tab-panel's own
   `display: flex` above would otherwise beat the UA stylesheet's `[hidden] { display: none }` at
   equal specificity, since this rule is declared later in the cascade. */
.profile-tab-panel[hidden] {
    display: none;
}

.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: 1rem;
}

.stat-grid[hidden] {
    display: none;
}

.stat-card {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    border-radius: 12px;
    padding: 1rem;
}

.stat-card__label {
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.stat-card__value {
    font-size: 1.75rem;
    font-weight: 700;
    color: var(--rb-text, #edeae3);
}

.stat-card__badge {
    margin: 0;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--rb-accent, #edeae3);
}

/* Accident stat on the (public, non-theme-reflecting) activity page -- a literal, fixed red, not
   var(--rb-accent): unlike .error's theme-tied red-ish fallback, this value must always read as
   red regardless of any future reuse on a themed page, since red here has a specific "something
   alarming happened" meaning (see CLAUDE.md's "Rider activity map" section) that a rider's own
   accent color could otherwise wash out or contradict. */
.stat-card__value--danger {
    color: #c62828;
}

/* Active-riders stat on the same (public, non-theme-reflecting) activity page (added 2026-09-11,
   direct request) -- a literal, fixed green matching the activity map's own green marker/legend/
   "active now" text, which is itself sampled directly from the logo's "-N-" lettering (#2ae323,
   confirmed via a direct pixel sample, not a guess) and matches site.css's own --rb-brand-green
   token. Fixed rather than var(--rb-accent) for the same reasoning --danger above already
   establishes: this value must always read as "the logo's green," not whatever accent a future
   themed reuse of this page might introduce. */
.stat-card__value--active {
    color: #2ae323;
}

.state-badges,
.mileage-badges {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin-top: 0.25rem;
}

/* .mileage-badge (roadmap item 17) mirrors .state-badge exactly -- same pill treatment, grouped
   together rather than duplicated so the two badge kinds can never visually drift apart. */
.state-badge,
.mileage-badge {
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    padding: 0.15rem 0.45rem;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--rb-text, #edeae3);
}

/* Forum (roadmap item 48, redesigned 2026-09-01 into a StackExchange-style Q&A -- see CLAUDE.md's
   Forum "Website frontend" subsection): sort tabs, category+tag filter row, new-thread composer,
   two-column thread list + sidebar, vote widgets, tags, accepted-answer highlighting, and the
   thread detail + flat replies. Same --rb-* / color-mix() token approach as Timeline/Messaging
   above -- no hardcoded colors (deliberately not the reference layout's blue scheme), so a
   rider's reflected theme (see "Rider website login & theme reflection" in CLAUDE.md) still
   applies here. */
/* Wider than the shared 640px page rule above -- a two-column thread-list + sidebar layout needs
   real room; this selector's own max-width here wins over the shared rule's on source order (same
   0,1,0 specificity), without needing to repeat every other property that rule already set. */
.forum-page {
    gap: 1.25rem;
    max-width: 1100px;
}

.forum-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    border-bottom: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    padding-bottom: 0.5rem;
}

.forum-tab {
    background: transparent;
    border: none;
    border-radius: 999px;
    padding: 0.4rem 0.9rem;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--rb-label, #a39fae);
    cursor: pointer;
}

.forum-tab:hover {
    background: color-mix(in srgb, var(--rb-text, #edeae3) 8%, transparent);
}

.forum-tab--active {
    color: var(--rb-accent, #edeae3);
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 16%, transparent);
}

.forum-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: 0.75rem;
}

/* Active tag facet chip -- shown alongside the pre-existing category <select> in the same filter
   row (folding the new tag facet into the existing toolbar, per the redesign spec, rather than a
   second separate filter bar). */
.forum-active-tag {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.85rem;
    color: var(--rb-text, #edeae3);
    border: 1px solid color-mix(in srgb, var(--rb-accent, #edeae3) 40%, transparent);
    border-radius: 999px;
    padding: 0.25rem 0.5rem 0.25rem 0.75rem;
}

.forum-active-tag[hidden] {
    display: none;
}

.forum-active-tag button {
    background: transparent;
    border: none;
    color: var(--rb-label, #a39fae);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    padding: 0 0.15rem;
}

.forum-active-tag button:hover {
    color: var(--rb-accent, #edeae3);
}

.forum-toolbar__field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.forum-toolbar__field select {
    font: inherit;
    color: var(--rb-text, #edeae3);
    background: transparent;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    padding: 0.35rem 0.5rem;
}

.forum-toolbar button,
.new-thread-form__row button[type="submit"] {
    background: transparent;
    color: var(--rb-accent, #edeae3);
    border: 1px solid var(--rb-accent, #edeae3);
    border-radius: 8px;
    padding: 0.5rem 1.1rem;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}

.new-thread-form {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    border-radius: 12px;
    padding: 1rem;
}

.new-thread-form[hidden] {
    display: none;
}

.new-thread-form__field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.new-thread-form__field input,
.new-thread-form__field select,
.new-thread-form__field textarea {
    font: inherit;
    color: var(--rb-text, #edeae3);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    padding: 0.4rem 0.6rem;
    resize: vertical;
}

.new-thread-form__row {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
}

.new-thread-form__row button[type="button"] {
    background: transparent;
    color: var(--rb-label, #a39fae);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 8px;
    padding: 0.5rem 1.1rem;
    font: inherit;
    cursor: pointer;
}

/* Two-column layout: main thread-list column + right sidebar (Top Users of the Week, Hot
   Questions). Stacks to one column under 960px -- see the media query near .forum-sidebar below.
   No ad-slot column here -- the reference layout's "Google Adsense 300x250" box is deliberately
   omitted (per the redesign spec), not stubbed out as dead space either. */
.forum-layout {
    display: grid;
    grid-template-columns: 1fr 280px;
    gap: 1.5rem;
    align-items: start;
}

.forum-main {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    min-width: 0;
}

.forum-sidebar {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

.forum-sidebar__widget {
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    border-radius: 12px;
    padding: 1rem;
}

.forum-sidebar__widget h2 {
    font-size: 0.95rem;
    margin: 0 0 0.6rem;
}

@media (max-width: 960px) {
    .forum-layout {
        grid-template-columns: 1fr;
    }
}

/* Top Users of the Week -- GET /api/forum/leaderboard/?window=week, capped at 10 by the backend.
   Medal styling on the top 3 ranks, a plain numeric rank badge below that. */
.leaderboard-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.leaderboard-list__item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.85rem;
}

.leaderboard-list__rank {
    flex: none;
    width: 1.5rem;
    text-align: center;
    font-family: var(--rb-mono-font);
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.leaderboard-list__item--medal-1 .leaderboard-list__rank,
.leaderboard-list__item--medal-2 .leaderboard-list__rank,
.leaderboard-list__item--medal-3 .leaderboard-list__rank {
    font-size: 1rem;
}

.leaderboard-list__name {
    flex: 1;
    color: var(--rb-text, #edeae3);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.leaderboard-list__points {
    flex: none;
    font-family: var(--rb-mono-font);
    font-size: 0.75rem;
    color: var(--rb-accent, #edeae3);
}

/* Hot Questions -- the top 5 threads from the backend's own ?sort=popular_week ranking (see
   forum.js's loadHotQuestions), a page-level widget fetched once, independent of whichever sort
   tab/filter is active in the main column. */
.hot-questions-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.hot-questions-list__item {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.5rem;
    font-size: 0.85rem;
}

.hot-questions-list__item a {
    color: var(--rb-text, #edeae3);
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.hot-questions-list__item a:hover {
    color: var(--rb-accent, #edeae3);
    text-decoration: underline;
}

.hot-questions-list__count {
    flex: none;
    font-family: var(--rb-mono-font);
    font-size: 0.75rem;
    color: var(--rb-label, #a39fae);
}

.forum-thread-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.forum-thread-list[hidden] {
    display: none;
}

/* Each card is a vote column (thread-level voting only here -- reply voting only appears on the
   detail page) beside a body column, rather than the whole card being one <a> -- the title is the
   only real navigation target now, so tag pills/author link/vote buttons inside the same card
   never have to fight a wrapping anchor for clicks (no nested-interactive-element workaround
   needed, unlike this card's pre-redesign author-link, which did need one). */
.forum-thread-card {
    display: flex;
    gap: 0.9rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    border-radius: 10px;
    padding: 0.75rem 0.9rem;
}

.forum-thread-card--solved {
    border-color: color-mix(in srgb, var(--rb-accent, #edeae3) 45%, transparent);
}

.forum-thread-card__body {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    min-width: 0;
}

.forum-thread-card__byline {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.8rem;
}

.forum-thread-card__link {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    color: var(--rb-text, #edeae3);
    text-decoration: none;
}

.forum-thread-card__link:hover .forum-thread-card__title {
    color: var(--rb-accent, #edeae3);
}

.forum-thread-card__title {
    font-weight: 600;
}

.forum-thread-card__solved-badge {
    font-size: 0.7rem;
    font-weight: 700;
    color: var(--rb-accent, #edeae3);
}

.forum-thread-card__stats {
    display: flex;
    flex-wrap: wrap;
    gap: 0.9rem;
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.forum-thread-card__timestamp,
.forum-thread-card__byline {
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.forum-category-badge {
    align-self: flex-start;
    border: 1px solid color-mix(in srgb, var(--rb-accent, #edeae3) 40%, transparent);
    color: var(--rb-accent, #edeae3);
    border-radius: 999px;
    padding: 0.1rem 0.55rem;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.03em;
    text-transform: uppercase;
}

/* Author-initial placeholder -- this codebase has no avatar-image field anywhere (mobile or
   website), so a plain accent-tinted initial stands in for the reference layout's avatar, same
   "no icon assets" convention as Timeline's emoji reaction glyphs. */
.avatar-placeholder {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 22%, transparent);
    color: var(--rb-accent, #edeae3);
    font-weight: 700;
    font-size: 0.75rem;
}

/* Review-pass fix (2026-09-08): same class-selector-vs-[hidden]-specificity gotcha as
   .avatar-image[hidden] below -- this base class was never previously toggled via the `hidden`
   attribute anywhere it was used (an inline post-author glyph, always rendered when there's no
   avatar), so this gap was latent until rider_profile.js/profile.js's new avatar preview reused
   it in a genuinely conditional show/hide context. Confirmed live: getComputedStyle was
   "inline-flex" with hidden === true before this fix, leaving the placeholder circle visible
   underneath/alongside a successfully-loaded real avatar image. */
.avatar-placeholder[hidden] {
    display: none;
}

/* Rider avatar (roadmap item 52 follow-up, added 2026-09-08): a real profile picture, shown on
   both the rider profile page (read-only) and the Profile settings page (editable preview).
   .avatar-placeholder--large is the "no avatar yet" fallback for both -- same accent-tinted
   initial-letter treatment as .avatar-placeholder above, just scaled up to headline size (this is
   no longer an inline post-author glyph). .avatar-row lays the image/placeholder out next to
   whatever sits beside it (the file input on Profile, nothing but itself on the rider profile
   page). */
.avatar-row {
    display: flex;
    align-items: center;
    gap: 1rem;
}

.avatar-image {
    display: block;
    width: 5rem;
    height: 5rem;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
}

.avatar-placeholder--large {
    width: 5rem;
    height: 5rem;
    font-size: 1.75rem;
}

/* Review-pass fix (2026-09-08): same class-selector-vs-[hidden]-specificity gotcha already
   documented several times in this file (.settings-form, .feed/.feed-skeleton, .chat-composer,
   etc.) -- .avatar-image's own `display: block` above beats the UA stylesheet's
   `[hidden] { display: none }` at equal specificity, since this rule is declared later in the
   cascade. Without this, a rider profile with no avatar set rendered a visible empty/broken image
   box (rider_profile.js/profile.js both correctly set the `hidden` attribute -- the DOM was right,
   only the CSS was silently ignoring it) alongside the correct initial-letter placeholder next to
   it. Confirmed live: getComputedStyle(img).display was "block" with img.hidden === true before
   this fix.*/
.avatar-image[hidden] {
    display: none;
}

/* Avatar crop/pan/zoom dialog (added 2026-09-11) -- same <dialog> shape as the Timeline compose
   dialog above (.compose-dialog/.compose-dialog::backdrop/.compose-dialog .composer): border/
   padding/background stripped off the <dialog> element itself (a <dialog> paints its own
   UA-default white panel otherwise), with an inner wrapper (.avatar-crop-panel) carrying the real
   background/padding/shadow -- kept as a genuinely separate rule set rather than sharing
   .compose-dialog's own classes, since this dialog is centered/fixed-size, not the composer's
   bottom-anchored/scrollable shape. */
.avatar-crop-dialog {
    border: none;
    padding: 0;
    background: transparent;
    max-width: min(92vw, 22rem);
    width: 100%;
    margin: auto;
}

.avatar-crop-dialog::backdrop {
    background: color-mix(in srgb, black 55%, transparent);
}

.avatar-crop-dialog[open] {
    display: block;
}

.avatar-crop-panel {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 1.5rem;
    box-sizing: border-box;
    background: var(--rb-bg, #15141a);
    border-radius: 12px;
    box-shadow: 0 12px 32px color-mix(in srgb, black 40%, transparent);
}

/* The circular "this is what your avatar will look like" mask comes entirely from this wrapper's
   own border-radius/overflow -- the <canvas> inside stays a plain square the whole time, JS never
   needs to clip anything itself. touch-action:none stops the browser's own default touch-scroll/
   pinch-zoom gestures from fighting the custom drag-to-pan handling (Pointer Events, unifying
   mouse and touch) in static/js/profile.js. cursor:grab/--dragging mirrors the drag affordance a
   rider would expect from any photo-crop tool. */
.avatar-crop-frame {
    width: 280px;
    height: 280px;
    max-width: 100%;
    align-self: center;
    border-radius: 50%;
    overflow: hidden;
    touch-action: none;
    cursor: grab;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
}

.avatar-crop-frame--dragging {
    cursor: grabbing;
}

.avatar-crop-frame canvas {
    display: block;
}

/* Tags -- shared between the thread-list card, the thread-detail page, and forum_tags.html's
   browse list (which additionally shows a thread_count via .tag-browse-list__count below). */
.tag-pill-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

.tag-pill-list[hidden] {
    display: none;
}

.tag-pill {
    display: inline-block;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    padding: 0.1rem 0.5rem;
    font-family: var(--rb-mono-font);
    font-size: 0.7rem;
    color: var(--rb-text, #edeae3);
    text-decoration: none;
}

.tag-pill:hover {
    border-color: var(--rb-accent, #edeae3);
    color: var(--rb-accent, #edeae3);
}

/* Shared vote widget -- thread-list cards (thread-level only), the thread-detail question, and
   every reply all render through the same forum.js renderVoteWidget()/handleVoteClick() pair (see
   that file), so the up/down-arrow markup only needs styling once here too. --sm is the compact
   variant used inside list cards and reply cards; the bare .vote-widget size is used for the
   thread-detail question, which gets a little more visual weight. */
.vote-widget {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
}

/* .vote-widget's own display:flex beats the UA [hidden] rule at equal specificity -- same
   class-selector-vs-[hidden] gotcha documented repeatedly elsewhere in this file (.feed/
   .feed-skeleton/.post-card__actions-menu/etc.). Needed for the thread-detail question's own
   persistent #thread-vote-widget element, toggled hidden for the thread's own author. */
.vote-widget[hidden] {
    display: none;
}

.vote-btn {
    background: transparent;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    width: 1.75rem;
    height: 1.75rem;
    line-height: 1;
    font-size: 0.85rem;
    color: var(--rb-label, #a39fae);
    cursor: pointer;
}

.vote-btn:hover {
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 14%, transparent);
    color: var(--rb-accent, #edeae3);
}

.vote-btn--active {
    border-color: var(--rb-accent, #edeae3);
    color: var(--rb-accent, #edeae3);
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 18%, transparent);
}

.vote-count {
    font-family: var(--rb-mono-font);
    font-weight: 700;
    font-size: 0.85rem;
    color: var(--rb-text, #edeae3);
}

.vote-widget--sm .vote-btn {
    width: 1.4rem;
    height: 1.4rem;
    font-size: 0.7rem;
}

.vote-widget--sm .vote-count {
    font-size: 0.75rem;
}

/* Forum thread detail page */
.forum-thread-page {
    gap: 1.25rem;
}

.thread-detail {
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    border-radius: 12px;
    padding: 1.25rem;
}

.thread-detail[hidden] {
    display: none;
}

.thread-detail__layout {
    display: flex;
    gap: 1rem;
}

.thread-detail__body-col {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    min-width: 0;
}

.thread-detail__meta {
    margin: 0;
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.thread-detail__body {
    margin: 0;
    color: var(--rb-text, #edeae3);
    white-space: pre-wrap;
    word-break: break-word;
}

.reply-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.reply-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    border-radius: 10px;
    padding: 0.75rem 0.9rem;
}

/* Accepted-answer highlight -- a visibly distinct border/background tint (accent-derived, no
   hardcoded color) plus the badge below, so the accepted reply reads as such even scrolled past
   its own badge. */
.reply-card--accepted {
    border-color: var(--rb-accent, #edeae3);
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 8%, transparent);
}

.accepted-answer-badge {
    align-self: flex-start;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    color: var(--rb-accent, #edeae3);
}

.reply-card__layout {
    display: flex;
    gap: 0.75rem;
}

.reply-card__body-col {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    min-width: 0;
}

.reply-card__meta {
    margin: 0;
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.reply-card__body {
    margin: 0;
    color: var(--rb-text, #edeae3);
    white-space: pre-wrap;
    word-break: break-word;
}

.reply-card__actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.2rem;
}

/* "Accept this answer" -- author-only, shown per not-yet-accepted reply (forum.js's
   isThreadAuthor()/isAccepted checks). Deliberately a plain outline button matching .delete-btn's
   weight, not a filled/primary treatment -- it sits right next to Delete on the rider's own reply
   and shouldn't visually outrank it. */
.accept-answer-btn {
    background: transparent;
    color: var(--rb-accent, #edeae3);
    border: 1px solid var(--rb-accent, #edeae3);
    border-radius: 6px;
    padding: 0.35rem 0.75rem;
    font: inherit;
    font-size: 0.8rem;
    font-weight: 600;
    cursor: pointer;
}

/* forum_tags.html -- the "Tags" nav destination's own page (see base.html's
   .app-header__forum-nav-link). */
.forum-tags-page {
    gap: 1rem;
}

.tag-browse-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.tag-browse-list[hidden] {
    display: none;
}

.tag-browse-list__item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    border-radius: 8px;
    padding: 0.6rem 0.9rem;
}

.tag-pill--browse {
    font-size: 0.85rem;
    padding: 0.2rem 0.7rem;
}

.tag-browse-list__count {
    font-family: var(--rb-mono-font);
    font-size: 0.75rem;
    color: var(--rb-label, #a39fae);
}

/* Help (added 2026-09-04) -- Documentation/Knowledge Base detail pages both render staff-authored
   HTML with |safe (see accounts/models.py's DocPage docstring for the trust-boundary reasoning).
   .help-content gives that arbitrary HTML (headings/lists/links/bold) reasonable, theme-token-based
   spacing without hardcoding any color -- everything else here (page layout, the article list,
   category badges) already comes from .static-page/.tag-browse-list/.forum-category-badge reused
   as-is, see each help_*.html template. */
.help-content {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    line-height: 1.6;
    color: var(--rb-text, #edeae3);
}

.help-content :is(h2, h3, h4) {
    margin: 0;
}

.help-content :is(ul, ol) {
    margin: 0;
    padding-left: 1.5rem;
}

.help-content a {
    color: var(--rb-accent, #edeae3);
}

/* Documentation page -- one .help-chapter section per distinct DocPage.chapter (a page with no
   chapter groups under "Uncategorized," see help_documentation_view/help_documentation.html).
   Mirrors .faq-category's own spacing exactly -- same "grouped list of staff-managed pages"
   shape, just linking out to a detail page (.tag-browse-list) instead of showing Q&A inline.
   The shared .static-page rule (site.css, ~L337) sets no gap of its own -- .help-documentation-
   page needs its own override the same way .timeline-page/.current-activity-page already do,
   or consecutive chapter sections render with zero space between them. */
.help-documentation-page {
    gap: 2rem;
}

.help-chapter {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.help-chapter__title {
    margin: 0;
}

/* FAQ page -- one .faq-category section per distinct FaqEntry.category (an entry with no category
   groups under "General," see help_faq.html's {% regroup %}). A plain <dl>, not .tag-browse-list,
   since a question/answer pair reads better as a definition list than a link row. */
.faq-category {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.faq-list {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.faq-list__item {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.faq-list__item dt {
    font-weight: 700;
    color: var(--rb-text, #edeae3);
}

.faq-list__item dd {
    margin: 0;
    color: var(--rb-text, #edeae3);
}

/* Dealer/retail directory (roadmap item 75, added 2026-09-29) -- a plain <ul>, not <dl>: each
   entry is a small card of independent fields (name/category/address/contact), not a single
   question/answer pair the way .faq-list's <dl> shape fits. Reuses .forum-category-badge as-is
   for the optional category pill (see dealer_directory.html) rather than inventing a near-
   duplicate badge class. */
.dealer-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.dealer-list__item {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    border-radius: 8px;
    padding: 0.75rem 0.9rem;
}

.dealer-list__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
}

.dealer-list__name {
    font-weight: 700;
    color: var(--rb-text, #edeae3);
}

.dealer-list__address {
    color: var(--rb-label, #a39fae);
}

.dealer-list__contact {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    font-size: 0.9rem;
}

.dealer-list__contact a {
    color: var(--rb-accent, #edeae3);
}

.delete-btn {
    background: transparent;
    color: var(--rb-accent, #edeae3);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    padding: 0.35rem 0.75rem;
    font: inherit;
    font-size: 0.85rem;
    cursor: pointer;
}

.delete-btn--small {
    margin-top: 0.4rem;
    padding: 0.2rem 0.55rem;
    font-size: 0.75rem;
}

.delete-btn[hidden] {
    display: none;
}

/* Profile page editable sections (Personal Information / Message Templates / Theme / Emergency
   Contacts, added 2026-08-31) -- same --rb-* / color-mix() token approach as every other page in
   this file, and the same .new-thread-form__field-style label+input pattern Forum already
   established, generalized into a reusable .settings-field so this page doesn't invent a fourth
   near-identical input style. */
.settings-form {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

/* Same class-selector-vs-[hidden]-specificity fix already documented several times in this file
   (.feed/.feed-skeleton/.composer__field/.load-earlier-btn/.chat-messages/.new-thread-form/
   .chat-composer/.delete-btn) -- .settings-form's own `display: flex` above would otherwise beat
   the UA stylesheet's `[hidden] { display: none }` at equal specificity, since this rule is
   declared later in the cascade. */
.settings-form[hidden] {
    display: none;
}

.settings-section {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    border-radius: 12px;
    padding: 1rem;
}

/* Same [hidden]-vs-class-selector-specificity gotcha documented several times elsewhere in this
   file (.feed/.feed-skeleton/.settings-form/.settings-field-row/.settings-field/etc.) -- caught
   live during the Groups redesign's own verification pass (2026-09-10): .settings-section's own
   `display: flex` above beat the UA stylesheet's `[hidden] { display: none }` at equal
   specificity, so group_detail.html's #group-switch-roles-section/#group-takeover-section/
   #group-bans-section (the first .settings-section elements in this repo ever toggled with the
   plain hidden attribute -- profile.html's own settings sections are always visible) rendered
   visible even with hidden=true set. Confirmed live via getComputedStyle before this fix (display:
   "flex" with hidden === true) and again after (display: "none"). */
.settings-section[hidden] {
    display: none;
}

.settings-section h2 {
    margin: 0;
    font-size: 1.1rem;
}

.settings-field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.settings-field input[type="text"],
.settings-field input[type="number"],
.settings-field input[type="date"],
.settings-field input[type="email"],
.settings-field input[type="password"],
.settings-field input[type="file"],
.settings-field select,
.settings-field textarea {
    font: inherit;
    color: var(--rb-text, #edeae3);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    padding: 0.4rem 0.6rem;
    resize: vertical;
}

/* A native <select> and a native <input type="date"> render their own internal (UA-drawn) content
   at different intrinsic heights even with identical padding/border/font -- matching padding alone
   (the shared rule above) isn't enough to make them the same height. Pinning both to an explicit
   height + box-sizing: border-box makes the box itself authoritative regardless of either control's
   own internal metrics. Scoped to just these two (not text/number/textarea, which weren't asked
   about and already look correct) to avoid changing anything beyond what this fixes. */
.settings-field select,
.settings-field input[type="date"] {
    height: 2.15rem;
    box-sizing: border-box;
}

.settings-field--checkbox {
    flex-direction: row;
    align-items: center;
    gap: 0.5rem;
    color: var(--rb-text, #edeae3);
    font-size: 0.9rem;
}

.settings-field--checkbox input[type="checkbox"] {
    width: 1rem;
    height: 1rem;
}

.settings-field--color input[type="color"] {
    width: 3.5rem;
    height: 2rem;
    padding: 0.1rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    background: transparent;
    cursor: pointer;
}

.settings-field-row {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
}

.settings-field-row > .settings-field {
    flex: 1 1 10rem;
}

/* Same [hidden]-specificity fix as .settings-form above -- #custom-theme-fields/#custom-theme-colors
   are toggled with the plain hidden attribute in profile.js's updateCustomThemeVisibility(). */
.settings-field-row[hidden] {
    display: none;
}

/* Same [hidden]-specificity fix, this time for a single .settings-field (not a whole row) toggled
   with the plain hidden attribute -- Groups redesign's #group-approval-type-field, hidden by
   groups.js whenever Visibility isn't currently Public (see updateApprovalTypeVisibility()). */
.settings-field[hidden] {
    display: none;
}

.settings-save-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.settings-save-row button {
    background: transparent;
    color: var(--rb-accent, #edeae3);
    border: 1px solid var(--rb-accent, #edeae3);
    border-radius: 8px;
    padding: 0.5rem 1.1rem;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}

.settings-save-row button:disabled {
    opacity: 0.6;
    cursor: default;
}

.settings-save-status {
    font-size: 0.85rem;
    color: var(--rb-text, #edeae3);
}

.settings-save-status--error {
    color: var(--rb-accent, #edeae3);
    font-weight: 600;
}

.contact-list {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.contact-list[hidden] {
    display: none;
}

.contact-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.75rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    border-radius: 10px;
    padding: 0.75rem;
}

.contact-row .settings-field {
    flex: 1 1 9rem;
}

.contact-row .settings-field--checkbox {
    flex: 0 0 auto;
}

/* Small meta text -- timestamps, category/type labels, post metadata -- gets IBM Plex Mono
   (visual polish pass, roadmap items 49-51): a technical, telemetry-adjacent feel fitting a
   motorcycle app, and a clear typographic distinction from body copy. --rb-mono-font is defined
   near the top of this file alongside --rb-body-font. */
.post-card__meta,
.thread-list__meta,
.forum-thread-card__byline,
.forum-thread-card__timestamp,
.forum-thread-card__stats,
.thread-detail__meta,
.reply-card__meta,
.state-badge,
.mileage-badge,
.forum-category-badge,
.composer__char-count,
.chat-bubble__time,
.stat-card__label {
    font-family: var(--rb-mono-font);
}

/* ==========================================================================
   App shell: persistent header + sidebar (roadmap items 49/50)
   ==========================================================================
   Fixed ~64px header (wordmark + account/logout) and fixed ~240px left sidebar (Timeline/
   Messages/Forum/Profile/Marketplace/Help), both position:fixed so they stay in place while the
   page scrolls under/beside them -- the normal document scroll is the only scroll container
   (no nested overflow div), matching this repo's preference for the simplest mechanism that
   works. .app-content offsets around both via margin. Collapses to a hamburger-triggered drawer
   under the mobile breakpoint (see the media query below) -- a bottom tab bar was considered and
   rejected: 6 items (including two disabled ones) reads cramped as icon-only tabs, and a drawer
   reuses the exact same nav markup/active-state CSS as desktop with no duplication. */
.app-shell {
    min-height: 100vh;
}

.app-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 64px;
    z-index: 40;
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0 1.25rem;
    background: var(--rb-shell-bg);
    border-bottom: 1px solid var(--rb-shell-border);
    box-sizing: border-box;
}

.app-header__menu-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: none;
    border-radius: 6px;
    background: transparent;
    color: var(--rb-text, #edeae3);
    cursor: pointer;
}

.app-header__menu-toggle:hover {
    background: color-mix(in srgb, var(--rb-text, #edeae3) 10%, transparent);
}

.app-header__menu-toggle svg {
    width: 1.5rem;
    height: 1.5rem;
}

/* Centered on the full screen width, not just in whatever space is left next to the hamburger
   toggle (2026-09-09 addendum, direct request: "We should center the logo on the screen as
   well."). The naive fix -- a flex layout that assumes .app-header__menu-toggle and a balancing
   spacer element are the same width -- is unreliable, since the toggle's real rendered width can
   drift from whatever value a spacer assumes (the sibling mobile app hit this exact bug and fixed
   it the same way, see that repo's own CLAUDE.md). The robust fix instead decouples the toggle and
   the logo entirely: .app-header is already position:fixed with left:0;right:0 (a full-viewport-
   width containing block), so absolutely positioning the brand link at left:50% with a -50%
   translateX centers it against the real viewport midpoint regardless of the toggle's width, the
   account link's text length, or whether the Forum/Help sub-nav is present. This removes the brand
   from the header's flex flow entirely -- .app-header__menu-toggle keeps its own flex position at
   the far left (unaffected), and .app-header__account's margin-left:auto still pushes it to the
   far right, both completely independent of where the logo lands. */
.app-header__brand {
    display: inline-flex;
    align-items: center;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
}

/* Sized to fit comfortably inside the 64px .app-header bar (see above) with room above/below. */
.app-header__brand-logo {
    display: block;
    height: 38px;
    width: auto;
}

/* Page-title header (added 2026-09-09) -- pages/templates/pages/partials/page_header.html,
   included only on the six main authenticated-app pages (Timeline, Messages, Friends, Groups,
   Forum, Search), each page's own <h1 class="chrome-text">.

   Bug-fix addendum (2026-09-09, same day): this originally paired the title with a second, newly-
   added logo <img> directly above it -- duplicating base.html's own fixed, persistent .app-header,
   which already renders the exact same logo on every authenticated page (see
   .app-header__brand-logo above). A rider saw the mark twice, stacked. Fixed by deleting the <img>
   from page_header.html entirely (see that file's own comment) -- .page-logo-header__logo (the
   image's own sizing rule) is removed here as dead code along with it. The persistent
   .app-header logo is the only one left on these pages.

   With the logo gone, this wrapper's only remaining job is the title itself, so `padding-top`
   replaces what the removed logo's own height + the old inter-item `gap` used to contribute:
   page containers (.timeline-page etc., see the shared page-container rule above) have no top
   padding of their own by design -- .app-content's padding-top: 64px already accounts for the
   fixed header's own height, so a page's content starts flush with zero extra gap -- which means
   without this the title would now sit flush against the persistent header bar with no visual
   separation. 1.5rem matches this file's existing spacing scale (the same value already used for
   the page container's own horizontal padding and .timeline-page's inter-section gap). */
.page-logo-header {
    display: flex;
    flex-direction: column;
    padding-top: 1.5rem;
}

.page-logo-header h1 {
    margin: 0;
}

/* Groups (the one of the six page_header.html-using pages whose top-of-page heading shares a row
   with an adjacent link -- "Find a group", see groups.html) -- a plain flex row pairing the title
   with that link. `align-items: baseline` (changed 2026-09-09 alongside the logo-removal fix
   above -- was `flex-end`, tuned for aligning against a now-removed, taller-than-text logo image;
   with only a bare <h1> left inside .page-logo-header, baseline is the correct alignment, matching
   .section-heading's own identical heading+link pattern used elsewhere on this and other pages).
   Kept as its own class rather than folded into .section-heading, since its child is
   .page-logo-header's own <h1> wrapper div, not a bare <h2> the way .section-heading expects. */
.page-logo-header-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.75rem;
    flex-wrap: wrap;
}

/* Forum Q&A redesign (2026-09-01) -- "Tags"/"Users" nav links + a quick-filter search box,
   integrated into the existing persistent app-header (rendered only on the three Forum pages, via
   base.html's request.resolver_match.url_name check) rather than a second, separate header. Sits
   between the brand and the account area, so it reads as an extension of the same header row. */
.app-header__forum-nav {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-left: 1.5rem;
    font-size: 0.9rem;
}

.app-header__forum-nav-link {
    color: var(--rb-label, #a39fae);
    text-decoration: none;
    white-space: nowrap;
}

.app-header__forum-nav-link:hover {
    color: var(--rb-text, #edeae3);
}

.app-header__forum-nav-link--active {
    color: var(--rb-accent, #edeae3);
    font-weight: 600;
}

.app-header__forum-search {
    font: inherit;
    font-size: 0.85rem;
    color: var(--rb-text, #edeae3);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 999px;
    padding: 0.3rem 0.75rem;
    width: 14rem;
    max-width: 40vw;
}

@media (max-width: 768px) {
    /* The header's Tags/Users/search row doesn't fit alongside the hamburger toggle + brand on a
       narrow viewport -- dropped rather than squeezed illegibly; Tags/Users are still reachable
       from the sidebar drawer's own Forum link, and the search box's job (filtering the loaded
       thread list) has no other affordance on mobile, a deliberate small scope cut for this pass. */
    .app-header__forum-nav {
        display: none;
    }
}

.app-header__account {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    margin-left: auto;
    font-size: 0.9rem;
}

/* The account name is a real link to /profile/ now (Profile was removed from the sidebar's
   account section and reached from here instead -- see CLAUDE.md's "App shell & typography"
   section). Plain text color + no underline so it doesn't look like a generic hyperlink sitting in
   the header chrome, with the same accent-tinted hover fill used for the "outline button" pattern
   elsewhere in this file (composer/chat/forum/settings-save buttons, see the shared hover rule near
   the top of this file) so it still reads as clickable. */
.site-nav__user {
    color: var(--rb-text, #edeae3);
    text-decoration: none;
    border-radius: 6px;
    padding: 0.3rem 0.5rem;
    margin: -0.3rem -0.5rem;
}

.site-nav__user:hover {
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 14%, transparent);
}

.app-shell-scrim {
    position: fixed;
    inset: 0;
    top: 64px;
    background: color-mix(in srgb, black 55%, transparent);
    z-index: 35;
}

.app-shell-scrim[hidden] {
    display: none;
}

.app-sidebar {
    position: fixed;
    top: 64px;
    bottom: 0;
    left: 0;
    width: 240px;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: 1rem 0.75rem;
    background: var(--rb-shell-bg);
    border-right: 1px solid var(--rb-shell-border);
    overflow-y: auto;
    z-index: 38;
    box-sizing: border-box;
}

.app-sidebar__link {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 0.75rem;
    border-radius: 8px;
    color: var(--rb-text, #edeae3);
    text-decoration: none;
    font-size: 0.95rem;
}

.app-sidebar__link:hover {
    background: color-mix(in srgb, var(--rb-text, #edeae3) 8%, transparent);
}

.app-sidebar__icon {
    flex: none;
    width: 1.25rem;
    height: 1.25rem;
}

/* Active nav item (matched per-page in base.html via request.resolver_match.url_name) uses the
   rider's accent color -- both the text/icon color and a left-edge indicator bar. */
.app-sidebar__link--active {
    color: var(--rb-accent, #edeae3);
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 14%, transparent);
    font-weight: 600;
}

.app-sidebar__link--active::before {
    content: "";
    position: absolute;
    left: -0.75rem;
    top: 0.35rem;
    bottom: 0.35rem;
    width: 3px;
    border-radius: 999px;
    background: var(--rb-accent, #edeae3);
}

/* Marketplace/Help -- no real page yet, shown disabled/"Coming Soon" rather than a dead link. */
.app-sidebar__link--disabled {
    color: var(--rb-label, #a39fae);
    cursor: default;
    opacity: 0.65;
}

/* Section break between the sidebar's three groups (primary / Forum+placeholders / account,
   added 2026-08-31) -- same visual weight as the divider that already sat above Log Out before
   this pass (see .app-sidebar__account-section below), now reused as a plain in-flow divider
   between Section 1 and Section 2 too. */
.app-sidebar__section-divider {
    border-top: 1px solid var(--rb-shell-border);
    margin: 0.5rem 0.75rem;
}

/* Section 3 (account: Profile + Log Out) -- pinned to the bottom of the sidebar via flex
   margin-top: auto on this wrapping div (not markup order), so it stays last even as more nav
   items are added to Section 1/2 above it. Carries the divider that used to sit directly on
   .app-sidebar__logout (Log Out was the only account-section item before this pass; Profile moved
   in here alongside it, out of the primary list, per the nav restructure). */
.app-sidebar__account-section {
    margin-top: auto;
    padding-top: 0.5rem;
    border-top: 1px solid var(--rb-shell-border);
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.app-sidebar__logout {
    display: contents;
}

.app-sidebar__link--button {
    width: 100%;
    background: none;
    border: none;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.app-sidebar__badge {
    margin-left: auto;
    font-family: var(--rb-mono-font);
    font-size: 0.65rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    padding: 0.15rem 0.4rem;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 25%, transparent);
    color: var(--rb-label, #a39fae);
}

.app-content {
    margin-left: 240px;
    padding-top: 64px;
    min-height: 100vh;
    box-sizing: border-box;
}

@media (max-width: 768px) {
    .app-header__menu-toggle {
        display: inline-flex;
    }

    .app-sidebar {
        width: min(80vw, 280px);
        transform: translateX(-100%);
        transition: transform 0.2s ease;
        box-shadow: 4px 0 16px color-mix(in srgb, black 35%, transparent);
    }

    .app-sidebar.is-open {
        transform: translateX(0);
    }

    .app-content {
        margin-left: 0;
    }
}

/* ==========================================================================
   Timeline compose FAB (roadmap item 51)
   ==========================================================================
   The composer used to sit at the top of the Timeline page and scroll with the feed. It now
   opens from a fixed circular button, bottom-right, using the rider's accent color -- genuinely
   fixed-position (no ancestor sets transform/filter, which would otherwise re-scope `position:
   fixed`), so it never moves during a feed scroll. The composer itself lives inside a native
   <dialog> (open/close mechanics in static/js/shell.js) rather than a hand-rolled modal --
   zero-dependency, and <dialog> gives real focus-trapping/Escape-to-close/top-layer stacking for
   free. */
.compose-fab {
    position: fixed;
    right: 1.5rem;
    bottom: 1.5rem;
    width: 3.5rem;
    height: 3.5rem;
    border-radius: 50%;
    border: none;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--rb-accent, #d32f2f);
    color: #fff;
    box-shadow: 0 6px 18px color-mix(in srgb, black 35%, transparent);
    cursor: pointer;
    z-index: 25;
}

.compose-fab svg {
    width: 1.5rem;
    height: 1.5rem;
}

.compose-fab:hover {
    filter: brightness(1.08);
}

.compose-dialog {
    border: none;
    padding: 0;
    background: transparent;
    max-width: min(92vw, 34rem);
    width: 100%;
    margin: auto;
}

.compose-dialog::backdrop {
    background: color-mix(in srgb, black 55%, transparent);
}

.compose-dialog[open] {
    display: block;
}

.compose-dialog .composer {
    max-height: 85vh;
    overflow-y: auto;
    box-sizing: border-box;
    background: var(--rb-bg, #15141a);
    box-shadow: 0 12px 32px color-mix(in srgb, black 40%, transparent);
}

.compose-dialog__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    margin-bottom: 0.5rem;
}

.compose-dialog__header h2 {
    margin: 0;
}

.compose-dialog__close {
    flex: none;
    border: none;
    background: transparent;
    color: var(--rb-label, #a39fae);
    width: 1.75rem;
    height: 1.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 6px;
    cursor: pointer;
}

.compose-dialog__close:hover {
    background: color-mix(in srgb, var(--rb-text, #edeae3) 10%, transparent);
}

.compose-dialog__close svg {
    width: 1.1rem;
    height: 1.1rem;
}

/* Timeline needs extra bottom clearance so the FAB never sits on top of the last feed card. */
.timeline-page {
    padding-bottom: 6rem;
}

/* ==========================================================================
   Friends / Groups / Search (added 2026-08-31) -- three new pages built against the
   already-shipped Friends & Groups backend, plus roadmap item 54's new ranked-search endpoint.
   Same --rb-* / color-mix() token approach as every other page in this file -- no hardcoded colors,
   so a logged-in rider's reflected theme still applies here. New page container classes
   (.friends-page/.groups-page/.groups-search-page/.group-detail-page/.search-page) are already
   listed in the shared bare-`main`-override selector above.
   ========================================================================== */
.friends-page,
.groups-page,
.groups-search-page,
.group-detail-page,
.search-page,
.rider-profile-page,
.moderation-page,
.moderation-riders-page,
.moderation-rider-detail-page {
    gap: 1.5rem;
}

/* Rider profile page (roadmap item 52) -- action area below the read-only stats (.stat-grid,
   reused verbatim from .profile-page above): one of "Send Request" / "Request Pending" /
   a "Send Message" link / informational text, never more than one at a time. */
.rider-profile-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.rider-profile-actions[hidden] {
    display: none;
}

/* Author-name links added to Timeline/Forum/Friends (roadmap item 52) -- a tap/click target that
   still reads as plain text inline in its existing card, not as a conspicuous "this is a link"
   blue underline; :hover is the only affordance, matching this file's existing restrained link
   styling elsewhere (e.g. .forum-thread-card__link). All four/five sites below share this rule so
   a future author-render site added to this file gets the identical treatment for free. */
.post-card__author-link,
.forum-thread-card__author-link,
.thread-detail__author-link,
.reply-card__author-link,
.rider-list__name-link {
    color: inherit;
    text-decoration: none;
}

.post-card__author-link:hover,
.forum-thread-card__author-link:hover,
.thread-detail__author-link:hover,
.reply-card__author-link:hover,
.rider-list__name-link:hover {
    text-decoration: underline;
}

/* Generic row list -- friends, friend requests, group members, join requests, group invites,
   search results. Deliberately not reusing .thread-list, since every row here needs an actions
   area (buttons), not just a single full-row <a> link like Messaging's conversation list. */
.rider-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.rider-list__item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    border-radius: 8px;
    padding: 0.6rem 0.85rem;
}

.rider-list__info {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    min-width: 0;
}

.rider-list__name {
    font-weight: 600;
    color: var(--rb-text, #edeae3);
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
}

.rider-list__meta {
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
}

.rider-list__actions {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    align-items: center;
}

/* Primary/positive action (Accept, Approve, Request to Join, Send Request, Create Group,
   Send Invite) -- identical accent-solid-border style to .forum-toolbar button/
   .new-thread-form__row button[type="submit"], reused here rather than re-declared a fourth time. */
.btn-accent {
    background: transparent;
    color: var(--rb-accent, #edeae3);
    border: 1px solid var(--rb-accent, #edeae3);
    border-radius: 8px;
    padding: 0.4rem 0.9rem;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
}

.btn-accent:disabled {
    opacity: 0.6;
    cursor: default;
}

/* Secondary/destructive action (Unfriend, Decline, Cancel, Reject, Leave Group, Remove Member) --
   reuses .delete-btn's exact muted-border style rather than a near-duplicate. */
.rider-list__actions .delete-btn {
    margin-top: 0;
}

/* Per-friend home-location-sharing toggle (roadmap item 67 follow-up, added 2026-09-01) --
   .settings-field--checkbox already supplies the checkbox+label look (see the Profile/Timeline
   Preferences pages); this just forces the toggle onto its own line within the flex-wrap
   .rider-list__item row rather than competing for space with the name/actions on the first line. */
.rider-list__share-toggle {
    flex-basis: 100%;
    margin-top: 0.25rem;
}

/* Terminal, non-interactive state text (e.g. Search's "Requested" after a successful send, or a
   400's returned detail message) -- same treatment as Timeline's post-report "Reported" swap. */
.rider-list__state {
    font-size: 0.85rem;
    color: var(--rb-label, #a39fae);
}

.rider-list__state--error {
    color: var(--rb-accent, #ff6b6b);
}

/* Reused across Friends/Groups/Search for the section-title + optional link/count row above a
   .rider-list. */
.section-heading {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.75rem;
    flex-wrap: wrap;
}

.section-heading h2 {
    margin: 0;
}

/* Explicit-submit search form (Search page + Groups search page) -- matches this codebase's
   established explicit-submit precedent (Group search already worked this way; no debounce
   anywhere in this repo) rather than introducing a first debounced-input pattern for this feature
   alone. */
.search-form {
    display: flex;
    gap: 0.5rem;
}

.search-form input[type="search"] {
    flex: 1;
    font: inherit;
    color: var(--rb-text, #edeae3);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 8px;
    padding: 0.5rem 0.75rem;
}

/* Dealer directory's category filter (roadmap item 75, added 2026-09-29) -- the first .search-form
   usage with a <select> instead of/alongside a search <input>; same look, same rule shape. */
.search-form select {
    font: inherit;
    color: var(--rb-text, #edeae3);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 8px;
    padding: 0.5rem 0.75rem;
}

/* Groups: "my groups" card grid + Create Group inline form (reuses .new-thread-form's exact
   field/row styling via the shared selectors below rather than a duplicate rule set). */
.group-card-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.group-card {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    border-radius: 10px;
    padding: 0.75rem 0.9rem;
}

.group-card__info {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    min-width: 0;
}

.group-card__name {
    font-weight: 600;
    color: var(--rb-text, #edeae3);
    text-decoration: none;
}

.group-card__name:hover {
    text-decoration: underline;
}

.group-card__meta {
    font-size: 0.8rem;
    color: var(--rb-label, #a39fae);
    display: flex;
    gap: 0.4rem;
    align-items: center;
    flex-wrap: wrap;
}

/* Group detail: description block + member/join-request/invite sections. */
.group-detail__description {
    color: var(--rb-text, #edeae3);
    white-space: pre-wrap;
    word-break: break-word;
}

/* Groups redesign (2026-09-10) -- Profile tab's "image left, description right" header row (spec
   item 8's "Top" row), a deliberate layout deviation from rider_profile.html's own stacked
   avatar-then-bio layout. .avatar-row (image/placeholder) is reused verbatim inside this wrapper
   rather than re-declared. */
.group-detail__header {
    display: flex;
    align-items: flex-start;
    gap: 1.25rem;
    flex-wrap: wrap;
}

.group-detail__header .group-detail__description {
    flex: 1 1 16rem;
    min-width: 0;
}

/* "Group name left, Leave Group right" row (spec item 8's "Below" row). */
.group-detail__title-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
}

.group-detail__title-row h1 {
    margin: 0;
}

/* Divider below the group name (spec item 11), token-styled rather than a bare UA-default <hr>. */
.group-detail__divider {
    border: none;
    border-top: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    margin: 0;
}

.group-detail__section {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

/* Same [hidden]-vs-class-selector-specificity gotcha as .settings-section[hidden] above -- caught
   live during the Groups redesign's own verification pass (2026-09-10) on the NEW
   #group-rules-text-section (group_rules.html), but this is a pre-existing gap: this class's
   `display: flex` above already beat the UA stylesheet's `[hidden] { display: none }` for the
   original #join-requests-section/#invite-section (group_detail.html, hidden from a non-admin
   member) too -- confirmed via getComputedStyle before this fix (display: "flex" with hidden ===
   true) and after (display: "none"). Fixed here rather than left as a second latent instance. */
.group-detail__section[hidden] {
    display: none;
}

/* Invite-a-rider form (admin-only) -- single email input + submit, same input styling as
   .new-thread-form__field's inputs and .search-form's search input above. */
.invite-form {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.invite-form input[type="email"] {
    flex: 1;
    min-width: 12rem;
    font: inherit;
    color: var(--rb-text, #edeae3);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 8px;
    padding: 0.5rem 0.75rem;
}

/* [hidden]-vs-class-selector specificity fix, same recurring reason documented several times
   above for .feed/.feed-skeleton/.composer__field/.load-earlier-btn/.chat-messages/
   .new-thread-form/.chat-composer/.delete-btn/.settings-form. */
.rider-list[hidden],
.group-card-list[hidden],
.new-group-form[hidden] {
    display: none;
}

.new-group-form {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    border-radius: 12px;
    padding: 1rem;
}

/* ==========================================================================
   Content moderation dashboard (added 2026-08-31)
   ==========================================================================
   Every token/technique below reuses the same --rb-* / color-mix() approach as the rest of this
   file -- no hardcoded palette, same as every other page, even though this dashboard is only ever
   seen by staff. Layout reuses .settings-section (bordered card), .new-thread-form__field (label +
   input), .rider-list, .section-heading, .btn-accent, .search-form, and .muted-note/.error rather
   than inventing near-duplicates -- only the pieces genuinely new to this feature (report queue
   cards, status badges, the messages-framework list) get their own rules here. */
.moderation-section {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.moderation-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.moderation-card {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 15%, transparent);
    border-radius: 10px;
    padding: 0.85rem 1rem;
}

.moderation-card__body {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.moderation-card__meta {
    font-size: 0.8rem;
    font-family: var(--rb-mono-font);
    color: var(--rb-label, #a39fae);
    margin: 0;
}

.moderation-card__title {
    font-weight: 600;
    color: var(--rb-text, #edeae3);
    margin: 0;
}

.moderation-card__content {
    color: var(--rb-text, #edeae3);
    white-space: pre-wrap;
    word-break: break-word;
    margin: 0;
}

/* Each reported item's inline Hide form -- a required textarea + submit, deliberately compact
   (this is a repeated-per-row control, not a standalone page section like Suspend/Ban below). */
.moderation-hide-form {
    display: flex;
    gap: 0.5rem;
    align-items: flex-start;
    flex-wrap: wrap;
}

.moderation-hide-form textarea {
    flex: 1;
    min-width: 12rem;
    font: inherit;
    color: var(--rb-text, #edeae3);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 6px;
    padding: 0.4rem 0.6rem;
    resize: vertical;
}

/* Suspend + Ban forms side by side on the rider-detail page -- both reuse .new-thread-form's
   existing field styling wholesale (see the shared selectors above), this just lays the two forms
   out next to each other rather than stacked. */
.moderation-action-forms {
    display: flex;
    gap: 1.5rem;
    flex-wrap: wrap;
    align-items: flex-start;
}

.moderation-action-forms .new-thread-form {
    flex: 1;
    min-width: 16rem;
}

/* Account status pill -- ACTIVE reads as unremarkable text, SUSPENDED/BANNED use the same
   accent-red already established for .error/.rider-list__state--error elsewhere in this file, so a
   restricted rider's status is visually consistent with every other "something needs attention"
   signal in this app, not a new color introduced just for this one badge. */
.status-badge {
    display: inline-block;
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    font-size: 0.75rem;
    font-weight: 600;
    font-family: var(--rb-mono-font);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    color: var(--rb-label, #a39fae);
}

.status-badge--suspended,
.status-badge--banned {
    color: var(--rb-accent, #ff6b6b);
    border-color: color-mix(in srgb, var(--rb-accent, #ff6b6b) 45%, transparent);
}

/* django.contrib.messages' framework-supplied success/error tags, rendered on the queue and
   rider-detail pages -- this feature's first real use of django.contrib.messages in this repo
   (installed/wired since day one, but never actually rendered in a template until now). */
.moderation-messages {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.moderation-messages li {
    border-radius: 8px;
    padding: 0.5rem 0.75rem;
    font-size: 0.9rem;
}

.moderation-messages li.success {
    color: var(--rb-text, #edeae3);
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--rb-accent, #edeae3) 35%, transparent);
}

.moderation-messages li.error {
    color: var(--rb-text, #edeae3);
    background: color-mix(in srgb, #ff6b6b 12%, transparent);
    border: 1px solid color-mix(in srgb, #ff6b6b 40%, transparent);
}

/* Login/Register/Forgot-Password/Reset-Password (redesigned 2026-09-04, direct request: "look
   more professional, center it horizontally, and lower it a little bit") -- all four pages share
   this one `.login-page` class and a bare `{{ form.as_p }}` render, which previously had zero
   form-specific CSS anywhere in this file (only the shared page-container rule's generic
   max-width/left-align applied), so every field rendered in raw browser-default styling. This
   rule block comes after that shared rule in the cascade, so it wins on source order at equal
   specificity -- same convention `.current-activity-page`/`.us-statistics-page` already use to
   widen past the shared 640px cap, just narrowing instead here. */
.login-page {
    max-width: 26rem;
    align-items: center;
    text-align: center;
    /* Centered horizontally via margin:auto (overriding the shared rule's plain `margin: 0 auto`
       is a no-op here since it's the same value -- the real fix was `align-items:center`/
       `text-align:center` above, since the shared rule's own `margin:0 auto` already centered the
       BOX, but its left-aligned text inside a fairly wide 640px box read as lopsided/off-center;
       narrowing the card AND centering its content together is what actually reads as centered).
       margin-top lowers it from the header, per the direct request, rather than sitting right
       under the nav. */
    margin-top: clamp(1.5rem, 6vh, 4rem);
    padding: 2rem 2rem 2.25rem;
    background: color-mix(in srgb, var(--rb-bg, #15141a) 78%, var(--rb-text, #edeae3) 6%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 14%, transparent);
    border-radius: 16px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.28);
}

.login-page h1 {
    font-size: 1.9rem;
}

.login-page .muted-note {
    margin-top: -0.25rem;
}

.login-page form {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    margin-top: 0.25rem;
}

.login-page form p {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    text-align: left;
}

.login-page form label {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--rb-label, #a39fae);
}

.login-page form input {
    font: inherit;
    font-size: 1rem;
    color: var(--rb-text, #edeae3);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 22%, transparent);
    border-radius: 8px;
    padding: 0.55rem 0.75rem;
    box-sizing: border-box;
    width: 100%;
}

.login-page form input:focus-visible {
    border-color: var(--rb-accent, #edeae3);
}

/* Solid/filled primary button -- a deliberate departure from this file's usual outline-button
   convention (.btn-accent/.settings-save-row button/etc): those are secondary actions inside an
   already-authenticated page; this is the single primary action on an anonymous entry point, and
   reads as more "professional"/decisive filled rather than outlined, per the direct request. Uses
   --rb-bg as the button's own text color (an inverse swap of the page's own bg/accent tokens,
   same "derive from existing tokens, no new hardcoded palette" discipline as every other themed
   rule in this file) since this app has no separate "text-on-accent" token. */
.login-page form button[type="submit"] {
    font: inherit;
    font-size: 0.95rem;
    font-weight: 700;
    color: var(--rb-bg, #15141a);
    background: var(--rb-accent, #edeae3);
    border: none;
    border-radius: 8px;
    padding: 0.65rem 1rem;
    cursor: pointer;
    margin-top: 0.35rem;
}

.login-page form button[type="submit"]:hover {
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 88%, white 12%);
}

.login-page a {
    color: var(--rb-accent, #edeae3);
}

.login-page a:hover,
.login-page a:focus-visible {
    text-decoration-thickness: 2px;
}

.login-page .moderation-messages {
    width: 100%;
}

/* Contact page (redesigned 2026-09-04, direct request: "center this and make it look more
   professional") -- same centered/lowered/shadowed-card treatment as .login-page just above, but
   sized for a couple of prose paragraphs rather than a form (wider, no form-field rules needed).
   `.contact-page` is layered on top of the shared `.static-page` class already on this page's
   <main> (see the page-container rule near the top of this file) so it keeps that rule's base
   flex/box-sizing plumbing and only overrides the specifics here -- same two-class pattern
   `.login-page` itself already uses. */
.contact-page {
    max-width: 32rem;
    align-items: center;
    text-align: center;
    margin-top: clamp(1.5rem, 6vh, 4rem);
    padding: 2.25rem 2.5rem 2.5rem;
    background: color-mix(in srgb, var(--rb-bg, #15141a) 78%, var(--rb-text, #edeae3) 6%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 14%, transparent);
    border-radius: 16px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.28);
}

.contact-page h1 {
    font-size: 1.9rem;
}

.contact-page a {
    color: var(--rb-accent, #edeae3);
}

.contact-page a:hover,
.contact-page a:focus-visible {
    text-decoration-thickness: 2px;
}

/* US Statistics page (ad-hoc test page, 2026-09-04) -- an interactive SVG map, sharing the
   .stat-grid/.stat-card/.muted-note conventions used across every other content page in this
   file. Wider than the shared 640px page-container cap, same pattern .current-activity-page
   already established for its own wide map image. Color values for both the marker dots and the
   three helmet-law fill categories live as custom properties on .us-statistics-page (not
   hardcoded per-rule) so the SVG's own fill and each legend swatch's background always agree --
   one source of truth, not two rules that could drift apart. */
.us-statistics-page {
    max-width: 1600px;
    gap: 1.5rem;
    --us-map-active: #2e7d32;
    --us-map-inactive: #1976d2;
    --us-map-accident: #c62828;
    --us-map-universal: #2e8b7f;
    --us-map-partial: #c98a2b;
    --us-map-none: #8d6e63;
}

/* Feature-request pipeline's public top-100 status page (roadmap item 3.12, added 2026-09-17) --
   a plain ranked table, not a card grid; wider than the default 640px cap (same reasoning as
   .current-activity-page/.us-statistics-page above) since a request's own message text needs room
   to breathe next to three status columns. */
.feature-requests-page {
    max-width: 900px;
    gap: 1.5rem;
}

.feature-request-table {
    width: 100%;
    border-collapse: collapse;
}

.feature-request-table th,
.feature-request-table td {
    text-align: left;
    padding: 0.6rem 0.75rem;
    border-bottom: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    vertical-align: top;
}

.feature-request-table th {
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    opacity: 0.75;
}

.feature-request-table__rank {
    width: 2.5rem;
    opacity: 0.6;
}

.feature-request-table__count {
    white-space: nowrap;
}

/* Reuses the exact same badge idiom .stat-card__badge/.stat-card__value--danger already
   established elsewhere in this file -- a status pill colored by meaning, not a free-standing new
   pattern. */
.feature-request-status--accepted,
.feature-request-status--shipped {
    color: #2e7d32;
}

.feature-request-status--rejected {
    color: #c62828;
}

.feature-request-status--pending,
.feature-request-status--not-started {
    opacity: 0.75;
}

/* The "floating" card treatment (direct request: the map "feels like it floats above the page")
   -- an elevated panel via box-shadow + a background a shade lighter than the page itself, rather
   than any scroll-triggered animation (this project's general restraint around motion, see
   CLAUDE.md's App shell & typography notes). position:relative scopes the absolutely-positioned
   zoom controls and hover popup layer to this frame, not the whole page. */
.us-map-frame {
    position: relative;
    /* overflow:hidden keeps a hover popup near the frame's own edge (e.g. Maine, Florida) clipped
       to the card itself rather than escaping visually onto the page content around it -- caught
       live during verification (a California popup rendered tall enough to overlap the intro
       paragraph above the frame before this was added). */
    overflow: hidden;
    border-radius: 16px;
    padding: 1.5rem;
    background: color-mix(in srgb, var(--rb-bg, #15141a) 82%, var(--rb-text, #edeae3) 6%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 12%, transparent);
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45), 0 4px 16px rgba(0, 0, 0, 0.3);
}

.us-map-frame__zoom-controls {
    position: absolute;
    top: 1.5rem;
    right: 1.5rem;
    z-index: 2;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.us-map-frame__zoom-btn {
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 8px;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    color: var(--rb-text, #edeae3);
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
}

.us-map-frame__zoom-btn:hover {
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 14%, transparent);
}

.us-map-frame__zoom-btn--reset {
    width: auto;
    padding: 0 0.6rem;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.us-map__svg {
    display: block;
    width: 100%;
    height: auto;
    cursor: grab;
    touch-action: none;
    /* Panning is dragged via JS-computed transform values, not this property -- but a smooth
       snap-back on the Reset button (and on the initial mount) reads better than an instant jump;
       JS toggles a "no-transition" class off during an active drag so live dragging itself stays
       1:1 with the pointer, not lagging behind a transition. */
}

.us-map__svg.us-map__svg--dragging {
    cursor: grabbing;
}

.us-map__zoom-layer {
    transition: transform 0.15s ease-out;
}

.us-map__svg--dragging .us-map__zoom-layer {
    transition: none;
}

.us-map__state {
    fill: color-mix(in srgb, var(--us-map-universal) 22%, var(--rb-bg, #15141a) 78%);
    stroke: color-mix(in srgb, var(--rb-text, #edeae3) 35%, transparent);
    stroke-width: 1.5;
    cursor: pointer;
    transition: fill 0.12s ease;
}

.us-statistics-page .us-map__state--universal {
    fill: color-mix(in srgb, var(--us-map-universal) 55%, var(--rb-bg, #15141a) 45%);
}

.us-statistics-page .us-map__state--partial {
    fill: color-mix(in srgb, var(--us-map-partial) 45%, var(--rb-bg, #15141a) 55%);
}

.us-statistics-page .us-map__state--none {
    fill: color-mix(in srgb, var(--us-map-none) 45%, var(--rb-bg, #15141a) 55%);
}

.us-map__state:hover,
.us-map__state--hover {
    fill: var(--rb-accent, #edeae3) !important;
}

.us-map__inset-frame {
    fill: none;
    stroke: color-mix(in srgb, var(--rb-text, #edeae3) 30%, transparent);
    stroke-width: 2;
}

.us-map__inset-label {
    fill: color-mix(in srgb, var(--rb-text, #edeae3) 60%, transparent);
    font-size: 30px;
    font-family: var(--rb-mono-font);
}

.us-map__marker {
    stroke: #fff;
    stroke-width: 1.5;
    pointer-events: none;
}

.us-map__marker--active {
    fill: var(--us-map-active);
}

.us-map__marker--inactive {
    fill: var(--us-map-inactive);
}

.us-map__marker--accident {
    fill: var(--us-map-accident);
}

.us-map-legends {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 1.5rem 3rem;
    /* .us-statistics-page is a left-aligned flex column (the shared page-container rule) -- this
       overrides that alignment just for the legends row so both groups (and the wrapped swatch
       rows within each, via .us-map-legend's own centering below) sit centered under the map,
       matching a direct request. */
    align-self: center;
    width: 100%;
}

.us-map-legend {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 0.4rem 1.25rem;
}

.us-map-legend h2 {
    width: 100%;
    text-align: center;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--rb-label, #a39fae);
}

.us-map-legend__item {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.85rem;
    color: var(--rb-text, #edeae3);
}

.us-map-legend__swatch {
    width: 0.85rem;
    height: 0.85rem;
    border-radius: 50%;
    border: 1px solid #fff;
    flex-shrink: 0;
}

.us-map-legend__swatch--active { background: var(--us-map-active); }
.us-map-legend__swatch--inactive { background: var(--us-map-inactive); }
.us-map-legend__swatch--accident { background: var(--us-map-accident); }
.us-map-legend__swatch--universal { background: var(--us-map-universal); border-radius: 3px; }
.us-map-legend__swatch--partial { background: var(--us-map-partial); border-radius: 3px; }
.us-map-legend__swatch--none { background: var(--us-map-none); border-radius: 3px; }

/* Hover popup -- built entirely by static/js/us_statistics.js, appended into #us-map-popup-layer
   (an absolutely-positioned, full-frame, pointer-events:none overlay so it never blocks map drag/
   hover) at the state's on-screen position: a small, ALWAYS-RECTANGULAR card carrying the actual
   text. An earlier cut also drew a second, decorative element clipped to the hovered state's own
   irregular outline (a literal "popup in the shape of the state") -- that element rendered mostly
   hidden behind this card and added little, so it was removed (2026-09-04, direct feedback:
   "doesn't look visually appealing"), not just left disabled. */
.us-map-popup-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 3;
}

.us-map__popup {
    position: absolute;
    width: 12.5rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 0.15rem;
    padding: 0.75rem;
    box-sizing: border-box;
    border-radius: 10px;
    background: color-mix(in srgb, var(--rb-bg, #15141a) 94%, var(--rb-text, #edeae3) 6%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
    color: var(--rb-text, #edeae3);
    opacity: 0;
    transition: opacity 0.12s ease;
}

.us-map__popup--visible {
    opacity: 0.97;
}

.us-map__popup__name {
    font-family: var(--rb-heading-font, inherit);
    font-size: 1rem;
    font-weight: 700;
    margin: 0 0 0.15rem;
}

.us-map__popup__stats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.05rem 0.6rem;
    font-size: 0.7rem;
    color: var(--rb-label, #a39fae);
}

.us-map__popup__stats strong {
    color: var(--rb-text, #edeae3);
}

.us-map__popup__helmet {
    margin-top: 0.35rem;
    font-size: 0.68rem;
    color: var(--rb-label, #a39fae);
    max-width: 12rem;
}

.us-map__popup__moto {
    font-size: 0.68rem;
    color: var(--rb-label, #a39fae);
}


/* ==========================================================================
   Phase 9: vehicle-icon indicators, unread-message badges, condensed reaction
   picker (added 2026-09-04) -- see CLAUDE.md's "Unread-message tracking" and
   "Active vehicle" sections (Website frontend parts) for the full rationale.
   ========================================================================== */

/* "Active vehicle" emoji badge -- appended next to a rider's name anywhere another rider is
   listed (Friends, Search/Riders Near You, Forum author bylines, Groups members, Messaging thread
   list). Plain emoji, not a color, since this codebase has no per-vehicle-type icon asset pipeline
   on the website (see the mobile app's own real SVG-derived vectors for the reason this isn't the
   same here) -- no new --rb-* token needed for that reason. The small left margin is redundant
   (harmless, not a bug) inside a flex container that already sets its own gap
   (.rider-list__name, .thread-list__name below) -- kept anyway so this badge still reads correctly
   spaced in a plain inline-flow context like a Forum byline that has no gap of its own. */
.vehicle-badge {
    display: inline-block;
    margin-left: 4px;
    font-size: 0.9em;
    line-height: 1;
}

/* Online/offline presence dot (added 2026-09-06) -- appended next to a rider's name on the
   Friends list and the Messages thread list. Absent entirely (not a "known offline" state --
   see friends.js/messaging.js) whenever the server's is_online is null, so a hidden/reciprocally-
   gated status renders as no indicator at all, never a false "offline" signal. Green means the
   real online state; a fixed semantic color (not var(--rb-accent)) so a rider's own theme accent
   can never wash out the one meaning this dot carries -- same reasoning
   .stat-card__value--danger/.post-card--crash-alert already established for this app's other
   fixed-meaning colors. */
.presence-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-left: 4px;
    background: #43a047;
    vertical-align: middle;
}

/* Friends page's own quick-filter input (added 2026-09-06) -- same visual language as
   .app-header__forum-search (pill-shaped, token-driven background/border), but this one lives in
   the page body, not the header, so it's full-width within its section rather than a fixed
   header-appropriate width. */
.rider-list-search {
    font: inherit;
    font-size: 0.9rem;
    color: var(--rb-text, #edeae3);
    background: color-mix(in srgb, var(--rb-bg, #15141a) 92%, var(--rb-text, #edeae3) 8%);
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 999px;
    padding: 0.4rem 0.9rem;
    width: 100%;
    max-width: 24rem;
    margin-bottom: 0.5rem;
}

/* Messaging thread list's name cell becomes inline-flex so the vehicle badge / unread badge sit
   inline with the rider's name at a consistent small gap, matching .rider-list__name's existing
   flex treatment elsewhere in this file. */
.thread-list__name {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
}

/* Per-thread unread-message badge (added 2026-09-04) -- a small filled pill next to a thread's
   rider name in the Messages thread list, shown only while thread.unread_count > 0 (see
   messaging.js's initThreadListPage()). Fixed white text on the accent fill, same precedent
   .compose-fab already established for an accent-background control needing reliably readable
   text regardless of which direction a rider's own theme skews -- not every color decision in this
   file needs a fresh --rb-* token when a direct existing precedent already covers the exact same
   accent-fill-plus-white-text case. */
.unread-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.1rem;
    height: 1.1rem;
    padding: 0 0.3rem;
    border-radius: 999px;
    background: var(--rb-accent, #edeae3);
    color: #fff;
    font-family: var(--rb-mono-font);
    font-size: 0.65rem;
    font-weight: 700;
    line-height: 1;
}

/* Sidebar Messages-menu unread-total badge (added 2026-09-04) -- reuses the existing
   .app-sidebar__badge base (the same class the Marketplace/Help "Soon" labels already use) plus
   this accent-filled modifier, so a rider immediately reads it as "count," not a second "Soon"-
   looking label. shell.js toggles the hidden attribute directly (never a class that sets
   display), but this explicit [hidden] override is kept anyway -- same belt-and-suspenders
   discipline as .post-card__actions-menu[hidden]/.reaction-picker__menu[hidden] below, since a
   class selector's display would otherwise beat the UA stylesheet's [hidden] rule at equal
   specificity if this rule ever gained one later. */
.app-sidebar__badge--unread {
    margin-left: 0.4rem;
    border: none;
    background: var(--rb-accent, #edeae3);
    color: #fff;
    text-transform: none;
    letter-spacing: normal;
}

.app-sidebar__badge--unread[hidden] {
    display: none;
}

/* Condensed reaction picker (item 79's website half, added 2026-09-04) -- shared structure for
   Timeline post/comment reactions (timeline.js's renderReactionButtons(), used by both
   .post-card__reactions and .comment-card__reactions) and DM reactions (messaging.js's
   .chat-bubble__reactions). Populated-only pills (.reaction-btn/.reaction-btn--active) render
   exactly as before, unchanged; this block styles only the new "+" trigger + dropdown that
   replaces the old always-visible six-button row. */
.reaction-picker {
    align-items: center;
}

/* .reaction-picker__empty (a quiet "No reactions yet" placeholder for a rider's own empty-reaction
   post/comment) was removed 2026-09-12 -- own content now fully hides the reaction container
   instead (see .post-card__reactions[hidden]/.comment-card__reactions[hidden] above), per the
   "these items are for other individuals" product decision, so there's no placeholder state left
   to style. */

.reaction-picker__control {
    position: relative;
}

.reaction-picker__trigger {
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 999px;
    background: transparent;
    color: var(--rb-label, #a39fae);
    width: 1.75rem;
    height: 1.75rem;
    line-height: 1;
    font-size: 1rem;
    cursor: pointer;
}

.reaction-picker__trigger:hover {
    background: color-mix(in srgb, var(--rb-text, #edeae3) 10%, transparent);
}

/* Same dropdown-card treatment as .post-card__actions-menu (position/border/background/shadow) --
   deliberately reused rather than a new visual language for what's the same kind of small
   click-to-open menu. */
.reaction-picker__menu {
    position: absolute;
    top: 100%;
    left: 0;
    margin-top: 0.25rem;
    display: flex;
    gap: 0.15rem;
    padding: 0.35rem;
    border: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 20%, transparent);
    border-radius: 8px;
    background: color-mix(in srgb, var(--rb-bg, #15141a) 96%, var(--rb-text, #edeae3) 4%);
    box-shadow: 0 4px 12px color-mix(in srgb, black 25%, transparent);
    z-index: 5;
}

.reaction-picker__menu[hidden] {
    display: none;
}

.reaction-picker__option {
    border: none;
    background: transparent;
    border-radius: 6px;
    padding: 0.25rem 0.4rem;
    font-size: 1rem;
    cursor: pointer;
}

.reaction-picker__option:hover {
    background: color-mix(in srgb, var(--rb-text, #edeae3) 10%, transparent);
}

/* Checkmark-marked active option (the emoji plus a checkmark character set in JS) also gets an
   accent tint, same treatment .reaction-btn--active already uses for a populated active pill. */
.reaction-picker__option--active {
    background: color-mix(in srgb, var(--rb-accent, #edeae3) 18%, transparent);
}

/* Password visibility ("eye") toggle (added 2026-09-06) -- companion CSS for
   static/js/password-toggle.js, which auto-discovers every input[type="password"] on the page
   (login/register/reset-password's {{ form.as_p }}-rendered widgets included) and wraps each in
   this markup itself, so this file needs no per-page selector, just this one small, generic
   utility. Kept deliberately minimal -- a raw `position: absolute` button rather than reaching for
   this file's usual --rb-* / color-mix() button-reset treatment, since this control is a plain
   icon toggle, not a themed action button; only its color is token-driven so it still reads
   correctly against any rider's theme. */
.password-toggle-wrapper {
    position: relative;
    display: inline-block;
    width: 100%;
}

.password-toggle-wrapper input[type="password"],
.password-toggle-wrapper input[type="text"] {
    padding-right: 2.5rem;
}

.password-toggle-btn {
    position: absolute;
    right: 0.5rem;
    top: 50%;
    transform: translateY(-50%);
    background: none;
    border: none;
    cursor: pointer;
    font-size: 1.1rem;
    line-height: 1;
    padding: 0.25rem;
    color: var(--rb-label, #a39fae);
}

/* ============================================================================================
   Midnight theme redesign (2026-09-09) -- a full visual redesign of the built-in "Midnight"
   theme only, from an approved standalone HTML mockup (not part of this repo). Originally gated
   purely on body[data-theme="midnight"] (base.html renders data-theme="{{ theme.id }}" from
   pages/theme.py's resolve_theme() -- see that file's comment for the fallback-id reasoning).
   Sunset/Forest/Custom are completely untouched by every rule below -- none of these selectors
   can ever match without one of the two attribute values below, so a rider on any other theme
   (including a Custom theme with colors that happen to resemble this palette) keeps rendering
   through the existing --rb-* token mechanism exactly as before this pass.

   UPDATED (2026-09-09, "Default becomes Midnight" pass): pages/theme.py's BUILT_IN_THEMES["default"]
   was reassigned to Midnight's own current color values -- a Lite/Free-safe, non-destructive way to
   retire the old light Default look without invalidating any account already stored with
   selected_theme_id="midnight" (which keeps resolving, unchanged, to data-theme="midnight" and this
   exact same visual result -- see CLAUDE.md's "Default becomes Midnight" addendum for the full
   reasoning). Reassigning the *color tokens* alone would NOT have made this section's own
   glow/pulse/chrome-text/control-color rules fire for a rider whose resolved theme is actually
   "default" -- every selector below was therefore broadened from a bare body[data-theme="midnight"]
   prefix to :is(body[data-theme="midnight"], body[data-theme="default"]), so the two theme ids are
   now full visual twins by construction, not just by coincidentally-equal token values. Plain Jane
   (BUILT_IN_THEMES["plain_jane"]) carries the OLD Default token values byte-for-byte and is NOT
   matched by anything in this section -- it renders through the plain --rb-* token mechanism only,
   exactly like Sunset/Forest, giving anyone who liked the original light Default look a way back to
   it under a new name. See CLAUDE.md's "Midnight theme redesign" section for the full design source
   and two judgment calls made implementing the original pass: how the near-black background is
   layered on top of --rb-bg rather than replacing the token mechanism, and how a "non-emergency
   system post" is detected (there is no dedicated post_type for one yet -- see
   static/js/timeline.js's SYSTEM_AUTHOR_EMAIL comment).
   ============================================================================================ */

/* Token overrides: --rb-bg/--rb-text/--rb-label are set directly on body[data-theme="midnight"],
   which wins over the inherited :root values from base.html's inline <style> block (a directly-
   declared value on an element always beats an inherited one, regardless of selector specificity
   or source order) -- so every existing --rb-bg/--rb-text/--rb-label consumer already in this
   file (post cards, the app shell, buttons, links, etc.) automatically re-themes to the approved
   dark palette with no per-component edits needed. --rb-label carries the mockup's "muted" tier
   (#a39da3) since that's the role this token already plays sitewide (post meta/timestamps, <p>
   text) -- closer to the existing app's own text hierarchy than forcing --rb-text and --rb-label
   to the identical value the mockup's prose literally shows for "body/label text" would have
   been. --rb-muted-2 (the mockup's "more-muted" #6f6a72 tier) is a new token with no existing
   sitewide role to slot into -- defined here for use by this section's own rules and left
   available for future Midnight-specific styling, not force-fit into unrelated components this
   pass. --rb-brand-green reuses the mobile app's own confirmed ShiftNTwistGreen sample
   (0xFF2AE323 / #2ae323, RideBuddy/.../ui/theme/Theme.kt on the sibling mobile repo, sampled
   directly from the logo mark's most common green pixel -- see that repo's CLAUDE.md "Logo glow
   removed..." section) rather than a second, independently-chosen green. */
:is(body[data-theme="midnight"], body[data-theme="default"]) {
    --rb-bg: #050506;
    --rb-text: #ece7e0;
    --rb-label: #a39da3;
    --rb-muted-2: #6f6a72;
    --rb-brand-green: #2ae323;
    /* --rb-shell-bg/--rb-shell-border (app header/sidebar chrome) are normally computed once, at
       :root/html, by the color-mix() rule near the top of this file -- html's own --rb-bg/
       --rb-text stay the ANONYMOUS-fallback values (base.html's data-theme attribute lives on
       body, not html, and a var() reference inside a property value always resolves against the
       cascaded value on the element the *declaring* rule matches, never a descendant's override),
       so without this, the shell chrome would silently stay light even once body/post-cards/etc.
       go dark. Redeclaring both directly here recomputes them from THIS rule's own already-dark
       --rb-bg/--rb-text above -- a direct declaration on body always wins over the inherited
       :root value regardless of specificity, same reasoning as this file's own
       "App shell & typography" --rb-shell-bg comment already documents for the light/dark-
       agnostic color-mix() trick itself, just applied one level down. Caught live during
       verification (2026-09-09): the header/sidebar rendered light-lavender against an otherwise
       black page before this fix. */
    --rb-shell-bg: color-mix(in srgb, var(--rb-bg) 90%, var(--rb-text) 10%);
    --rb-shell-border: color-mix(in srgb, var(--rb-text) 14%, transparent);
    /* Tells the browser this page is a dark UI so it renders NATIVE form-control chrome (a
       <select>'s own dropdown popup, checkbox/radio default rendering, date-picker calendar,
       scrollbar track/thumb) in its dark variant instead of the OS/browser light default. Nothing
       in this file overrides that native chrome itself (selects/checkboxes here are just plain
       elements with a border/background applied around their native structure, not
       appearance:none rebuilds), so without this a rider on Midnight got a jarring light-mode
       popup the instant they opened any <select> -- confirmed live opening Timeline Preferences'
       "Show" filter dropdown, which rendered a plain white options list with the OS-default blue
       highlight, completely unthemed against the black page underneath it. Scoped to this theme
       only, same as every other rule in this section -- Default/Sunset/Forest/Custom are already
       light UIs and must keep the light native-control rendering they already correctly get. */
    color-scheme: dark;
}

/* The body-level color-scheme: dark above was NOT enough on its own for a <select>'s dropdown
   POPUP specifically -- caught live (2026-09-09): getComputedStyle confirmed the <select> element
   itself correctly inherited color-scheme: dark from body, yet its opened popup still rendered
   with the plain light/OS-default chrome. Root cause: unlike ordinary inherited CSS properties,
   the browser decides a native <select> popup's (and the scrollbar's) color scheme from the
   ROOT <html> element's used color-scheme, not a descendant's -- and html's own color-scheme
   stayed "normal" here, since base.html's data-theme attribute is deliberately rendered on body,
   not html (same "declared on body, html doesn't know about it" shape as the --rb-shell-bg bug
   above, just for a browser-native feature instead of a CSS custom property). html has no
   data-theme of its own to select on, so this reaches it via :has() instead of a plain descendant
   override -- html:has(body[data-theme="midnight"]) matches only when this exact rider's theme
   attribute exists anywhere in the document, same effective scoping guarantee as every other rule
   in this section. Confirmed live afterward: the "Show" filter's popup opened with a dark
   background and light text, no more plain white list. */
html:has(:is(body[data-theme="midnight"], body[data-theme="default"])) {
    color-scheme: dark;
}

/* Flat black + a very subtle top-of-page vignette, layered as a second background-image on top of
   the plain --rb-bg fill above (body {} still supplies background: var(--rb-bg, ...) -- this rule
   only adds the gradient layer and wins the background-image cascade at higher specificity). */
:is(body[data-theme="midnight"], body[data-theme="default"]) {
    background-image: radial-gradient(ellipse 900px 600px at 50% -10%, #0d0b10 0%, transparent 60%);
    background-repeat: no-repeat;
    background-color: #050506;
}

/* Regular Timeline post cards ("message posts") -- semi-transparent glass over the black
   background. .post-card's own base rule (border/border-radius/padding, near the top of this
   file) still applies underneath; this only overrides background/border/radius and adds the blur.
   Higher specificity than .post-card--crash-alert/.post-card--system-notice's UNSCOPED base rules
   elsewhere in this file (those have no data-theme condition), but LOWER than this same section's
   own crash-alert/system-notice overrides below, which is exactly the layering the approved
   mockup calls for -- crash/system cards start from this same translucent gradient as their third
   background stop, then add their own tint on top. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .post-card {
    background: linear-gradient(180deg, rgba(236, 231, 224, 0.045) 0%, rgba(236, 231, 224, 0.02) 100%);
    border: 1px solid rgba(236, 231, 224, 0.10);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border-radius: 20px;
}

/* Crash-alert posts (this app's existing .post-card--crash-alert extension point, defined near
   the top of this file for every theme -- CRASH_ALERT is always authored by the fixed system
   account, see accounts/crash_alerts.py's get_or_create_system_author()) -- red, glowing,
   pulsing, replacing the plain color-mix() red tint with the approved mockup's exact treatment
   for this one theme. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .post-card--crash-alert {
    background: linear-gradient(180deg, rgba(255, 59, 59, 0.13), rgba(255, 59, 59, 0.03) 65%, rgba(236, 231, 224, 0.02));
    border-color: rgba(255, 59, 59, 0.55);
    animation: pulse-red 2.6s ease-in-out infinite;
}

/* Other system-authored, non-emergency posts -- same red/glow/pulse treatment in the brand green
   instead. JUDGMENT CALL (see CLAUDE.md): Post.POST_TYPE_CHOICES has no dedicated non-emergency
   "system" type today (only GENERAL/GROUP_ANNOUNCEMENT/RIDE_ACTIVITY/CRASH_ALERT exist) -- a
   system-authored GENERAL post is indistinguishable from a real rider's GENERAL post by
   post_type alone. This class is applied by static/js/timeline.js keyed on the post's author
   being the same fixed system account CRASH_ALERT posts use (post.author.email ==
   SYSTEM_AUTHOR_EMAIL) with post_type != CRASH_ALERT as the differentiator from the red case
   above -- not a post_type value, since none exists yet for this. No non-emergency system post
   has ever shipped in this codebase's content, so this class currently has no live example to
   render against outside a manually-seeded demo post -- see CLAUDE.md for how that was verified. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .post-card--system-notice {
    background: linear-gradient(180deg, rgba(42, 227, 35, 0.10), rgba(42, 227, 35, 0.025) 65%, rgba(236, 231, 224, 0.02));
    border-color: rgba(42, 227, 35, 0.45);
    animation: pulse-green 2.6s ease-in-out infinite;
    /* Offset by half the 2.6s cycle so a red and green pulsing card visible in the same feed
       scroll don't beat in visual sync -- explicit follow-up request after the first draft. */
    animation-delay: -1.3s;
}

@keyframes pulse-red {
    0%, 100% {
        box-shadow: 0 0 0 1px rgba(255, 59, 59, 0.22), 0 0 26px 2px rgba(255, 59, 59, 0.28), 0 0 60px 6px rgba(255, 59, 59, 0.10);
    }
    50% {
        box-shadow: 0 0 0 1px rgba(255, 59, 59, 0.38), 0 0 40px 6px rgba(255, 59, 59, 0.42), 0 0 90px 14px rgba(255, 59, 59, 0.16);
    }
}

@keyframes pulse-green {
    0%, 100% {
        box-shadow: 0 0 0 1px rgba(42, 227, 35, 0.18), 0 0 26px 2px rgba(42, 227, 35, 0.24), 0 0 60px 6px rgba(42, 227, 35, 0.09);
    }
    50% {
        box-shadow: 0 0 0 1px rgba(42, 227, 35, 0.32), 0 0 40px 6px rgba(42, 227, 35, 0.36), 0 0 90px 14px rgba(42, 227, 35, 0.14);
    }
}

@media (prefers-reduced-motion: reduce) {
    :is(body[data-theme="midnight"], body[data-theme="default"]) .post-card--crash-alert,
    :is(body[data-theme="midnight"], body[data-theme="default"]) .post-card--system-notice {
        animation: none;
        /* Keep the static (0%) glow rather than dropping the box-shadow entirely -- the alert
           still needs to read as visually distinct at a glance, just without motion. */
        box-shadow: 0 0 0 1px rgba(255, 59, 59, 0.22), 0 0 26px 2px rgba(255, 59, 59, 0.28), 0 0 60px 6px rgba(255, 59, 59, 0.10);
    }
    :is(body[data-theme="midnight"], body[data-theme="default"]) .post-card--system-notice {
        box-shadow: 0 0 0 1px rgba(42, 227, 35, 0.18), 0 0 26px 2px rgba(42, 227, 35, 0.24), 0 0 60px 6px rgba(42, 227, 35, 0.09);
    }
}

/* Chrome-text heading treatment -- reuses this app's own app-icon chrome visual language (a
   multi-band brushed-metal gradient-text effect), applied via a plain .chrome-text class added
   directly in each template. Originally (2026-09-09) scoped to just the seven primary nav-level
   page <h1>s (Timeline/Profile/Forum/Messages/Friends/Groups/Search); widened the same day, same
   pass, to every genuine heading-level element across the authenticated shell -- every other
   page-level <h1> that was missed the first time (Timeline Preferences, Chat Thread, Group
   Detail, Groups Search, Forum Thread, Forum Tags, Rider Profile), every real section-level <h2>
   on those pages and their sub-pages (e.g. Profile's "Personal Information"/"Emergency Contacts"/
   etc., Timeline Preferences' four section headers, Friends/Groups/Messages/Forum's list-section
   headers, Forum's sidebar widget headers), and the compose dialog's "New Post" heading (a real h2
   heading with the same var(--rb-bg) background as the page itself, no legibility reason to treat
   it differently from any other h2). Deliberately still NOT applied to body copy, labels, button
   text, metadata/timestamp text, or repeating per-item list/card text (an individual forum
   thread's own title, a rider's name in a list, a group card's name, a conversation's name) --
   those are list content, not headings, and chroming dozens of small repeated inline spans would
   be visually noisy rather than legible. See CLAUDE.md's dated addendum for the full list. The
   rule itself is scoped to body[data-theme="midnight"] .chrome-text, not bare .chrome-text -- the
   gradient's light tones are tuned against this theme's near-black background specifically and
   would be nearly illegible against Default/Sunset/Forest's light backgrounds, so the class is
   harmless (renders as an ordinary heading) on every other theme rather than being conditionally
   added per-theme in the template itself.

   Gradient re-tuned to real logo colors (2026-09-09 bug-fix addendum): the 9-stop gradient below
   was originally hand-authored -- a plausible-looking cool blue-grey metal gradient, but not
   actually sampled from this app's own logo artwork, so it drifted from what the real mark's chrome
   looks like. Rebuilt from real pixels sampled out of static/images/shiftntwist-logo-header.png
   (Python + Pillow, throwaway script, not checked in): every fully-opaque pixel (alpha==255, which
   already excludes the logo's own soft ambient drop-shadow -- baked in as a semi-transparent halo,
   see "Logo refresh" above) inside the "Shift"/"Twist" lettering was collected, then eroded ~9px
   inward from every edge to additionally exclude the thin dark keyline traced at the mark's own
   silhouette (also part of the "Logo refresh" pass's "Pop" treatment) and ordinary antialiasing --
   leaving only well-interior metal pixels. The bright green "-N-" was excluded by hue (not a
   bounding box -- more robust against its irregular glyph shape), and the handle-grip/brake-lever
   hardware icons (also chrome-colored, but not lettering) were excluded by explicit pixel
   bounding boxes. Sorting the remaining ~1,200-2,300-pixel-per-band sample set by perceptual
   luminance and averaging real percentile bands gave a genuine tonal ramp -- a cool near-white
   highlight (~#F6F3FA-#FAF8FC), a warm mauve-grey mid-tone (~#B2A4B0), and a warm dark shadow
   (~#6B5C67-#7F707B) actually present in the artwork -- confirmed, not assumed: the real logo's
   metal reads distinctly warmer/pinker than the old gradient's cool blue-grey, verified via a
   side-by-side raster comparison (logo crop directly above rendered swatches of both gradients)
   before this change was made, not just by eyeballing hex codes. The new gradient below keeps the
   same stop count/positions/100deg angle as the original (same alternating light/dark banding
   structure, mimicking the same brushed-metal environment-reflection look) -- only the actual hex
   values were replaced, with real sampled tones substituted in at matching relative-brightness
   ranks (not a mechanical 1:1 copy of exactly 3 sampled pixels): the two near-white bands (10%/72%)
   and the brightest highlight peak (34%) use real highlight-band samples; 46%/86%/100% use real
   mid/lower-mid/upper-mid band samples; 0%/22% use real shadow-band samples; and 58% (the gradient's
   darkest trough, for contrast/depth) uses a slightly darker real sample from a lower-erosion pass
   still confirmed to sit within the opaque letter interior, not the excluded outline. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .chrome-text {
    /* Angle fixed to true horizontal, 2026-09-09 addendum: was 100deg. In CSS gradient-angle
       convention (0deg = up, 90deg = right/horizontal, clockwise), 100deg carries a small but real
       non-zero vertical component (cos(100deg) ~= -0.174, ~17% of the horizontal component) --
       invisible averaged across a whole word's width, but a visible top-bright/bottom-dark fade
       within a single narrow glyph stroke (the vertical stems in "T"/"i"/"l"), which is exactly
       what reads as "vertical" even though the gradient is nominally "mostly horizontal". The
       mobile app's ChromeText hit the identical bug (its own 100deg Compose angle) and was fixed
       the same way -- see that repo's own CLAUDE.md. 90deg ("to right") mathematically zeroes the
       vertical component for any glyph; this horizontal layer's own stops/colors are unchanged
       from the just-completed re-color pass and STAY that way below -- this is the "reflection
       sweeping across the word" effect, already correct and already verified live.

       Second layer + per-glyph vertical bevel, added same day, same pass, per direct follow-up
       feedback on the 90deg fix above: "the top & bottom of the heading text [should be] shaded
       with the center of the text being more highlighted -- the same way the logo is." This is a
       genuinely different, orthogonal effect from the horizontal sweep (a per-glyph dark-top/
       bright-middle/dark-bottom bevel, not a left-right color band) and needed its own gradient
       layer, not a tweak to the one above. Checked against the real artwork first, not assumed:
       a throwaway Python+Pillow script (not checked in) sampled real luminance down the centerline
       of 5 clean vertical strokes in static/images/shiftntwist-logo-header.png (the stems of
       Shift's "h"/"f"/"t" and Twist's "T"/"i"), tracking each stroke's own local centerline
       per-row (not a fixed screen-space x-column) to correct for the italic slant -- a naive
       fixed-column sample cuts across a diagonal stroke at a different left/right position as y
       changes, contaminating a "vertical" reading with the horizontal cross-stroke bevel. Real
       measured profile (perceptual luminance 0-255, averaged across the 5 strokes, at 10/25/50/
       75/90% of each stroke's own clean-shaft height): 216 / 210 / 158 / 129 / 144 -- genuinely
       non-flat (a real ~40% swing), confirming a real vertical effect belongs here, but the
       artwork's own actual shape is NOT a symmetric dark/bright/dark peak-in-the-exact-middle --
       it reads as bright-at-top, darkening through a trough around 70-80% down the stroke, then a
       partial (not full) rebound right at the very base, consistent with this specific asset's own
       single upper-left-ish key light rather than a perfectly even two-sided bevel. Reported to
       the user as-measured, not fudged to fit the hypothesis -- but the user's own explicit,
       literal, twice-independently-given request (identical reasoning also just applied to the
       mobile app's ChromeText.kt) is the authoritative design goal regardless of this one PNG's
       exact specular quirk, and a symmetric dark/bright/dark bevel is the standard, convincing way
       to render extruded chrome lettering in general -- so that's what's implemented below, using
       only real colors already sampled from the logo (no new invented hex values): the same
       darkest-real-metal shadow (#6b5c67) and near-white highlight (#f9f8fc) already anchoring the
       horizontal gradient above, arranged as a 3-stop symmetric vertical ramp.

       Compositing: `background-image` takes a comma-separated list, so the vertical gradient is a
       second layer alongside the horizontal one, combined via `background-blend-mode`. Three modes
       were actually rendered and compared by eye (a local canvas/CSS test page, not a guess) against
       both each other and the real logo: `multiply` measurably shapes the glyph (a real ~27pt
       luminance swing in a canvas-pixel test replicating this exact composite) but reads a little
       muddy/muted; `screen` washes out the horizontal sweep's own contrast almost entirely (~27pt
       swing, everything crowds toward near-white); `overlay` gave the strongest, cleanest shaping
       (~52pt swing) while staying vivid and legible, not muddy -- confirmed via real getImageData
       pixel sampling of a canvas recreation of this exact composite (same gradients, same
       globalCompositeOperation, which uses the identical blend-mode math as CSS
       background-blend-mode), not just eyeballing: sampling a "T" glyph's clean vertical stem at
       35/50/65/85% of its height gave L=190.2/217.1/242.9/212.4 for overlay (darker top and
       bottom, brighter middle, exactly the requested shape) vs. a flat L~128.6 at every one of
       those same rows for the sweep-only baseline (proving the vertical layer is doing real,
       measurable work, not a no-op). `overlay` is the one shipped below. */
    background-image: linear-gradient(90deg, #837480 0%, #f9f8fc 10%, #7f707b 22%, #fcfafe 34%, #b2a4b0 46%, #6b5c67 58%, #faf8fc 72%, #998996 86%, #d1c5d1 100%),
                       linear-gradient(180deg, #6b5c67 0%, #f9f8fc 50%, #6b5c67 100%);
    background-blend-mode: overlay;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.65));
}

/* Nav/menu surfaces re-accented in the brand green (2026-09-09 follow-up: "All of the menu
   options should reflect the theme as well.") -- ROOT CAUSE, same class of bug as the
   --rb-shell-bg fix above: this theme's token-override block only ever redeclared --rb-bg/
   --rb-text/--rb-label (+ the two brand-new tokens). --rb-accent was never part of that list, so
   every consumer of var(--rb-accent) sitewide -- including the sidebar's active-item highlight,
   the header account-link hover, and the Forum/Help header sub-nav's active-tab color -- kept
   rendering this theme's OLD built-in accent (#5e5ce6, an indigo chosen for the light BEFORE-
   redesign Midnight palette, see pages/theme.py's BUILT_IN_THEMES) instead of anything from the
   new black+green mockup. Confirmed live via getComputedStyle before this fix:
   .app-sidebar__link--active resolved to rgb(94, 92, 230) (that exact indigo), not the brand
   green. Deliberately scoped to just these nav-shaped selectors, not a blanket
   --rb-accent override on body[data-theme="midnight"] -- this app's ordinary content buttons/
   links/tags/vote-arrows/etc. also read var(--rb-accent) sitewide, and re-tinting every one of
   those to green was never asked for and was outside this pass's "menu options" scope; only the
   nav/menu surfaces themselves change here. The mobile hamburger drawer needs no separate rule --
   it reuses this exact same .app-sidebar markup/CSS at a narrower viewport (see the "App shell &
   typography" section's responsive bullet), so it re-themes for free. There is no separate
   account/profile dropdown menu in this app to fix -- .site-nav__user (the header's account-name
   control) is a single link straight to /profile/, not a menu with its own popup -- covered here
   anyway since its :hover fill was the same stale-indigo tint. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .app-sidebar__link--active {
    color: var(--rb-brand-green);
    background: color-mix(in srgb, var(--rb-brand-green) 14%, transparent);
}

:is(body[data-theme="midnight"], body[data-theme="default"]) .app-sidebar__link--active::before {
    background: var(--rb-brand-green);
}

:is(body[data-theme="midnight"], body[data-theme="default"]) .site-nav__user:hover {
    background: color-mix(in srgb, var(--rb-brand-green) 14%, transparent);
}

/* Sidebar Messages-menu unread-count badge -- same stale-indigo bug (.app-sidebar__badge--unread's
   base rule, added 2026-09-04, fills with var(--rb-accent) + fixed white text). Re-accented to the
   brand green, but with --rb-bg (near-black) text instead of carrying over the base rule's fixed
   white -- white-on-#2ae323 measures under 2:1 contrast (the brand green is too luminant a
   background for light text), while --rb-bg on it measures well over 10:1. Every other theme's
   accent (#5e5ce6 indigo, #ff7043 sunset orange, #00695c forest teal) is dark enough that white
   text over it already reads fine, which is why the base rule could get away with a fixed color;
   this override only needed to happen for this one unusually bright accent. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .app-sidebar__badge--unread {
    background: var(--rb-brand-green);
    color: var(--rb-bg);
}

/* .app-header__forum-nav-link--active is shared verbatim by both the Forum header sub-nav
   (Tags/Users) and the Help header sub-nav (Documentation/Knowledge Base/FAQ) -- see base.html's
   comment on why Help reuses this class rather than a near-duplicate rule set -- so one override
   here re-themes both. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .app-header__forum-nav-link--active {
    color: var(--rb-brand-green);
}

/* Focus-visible rings on the nav/menu controls themselves (sidebar links, the hamburger toggle,
   the header account link, the Forum/Help sub-nav links, and every <select>) in the brand green,
   matching the same active-state re-accent above -- the sitewide a/button/input/textarea/select
   focus-visible rule (near the top of this file) still supplies the plain --rb-accent ring
   everywhere else (ordinary buttons/links/inputs), which was never in this pass's "menu options"
   scope; only nav-shaped and select controls get the green ring under Midnight. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .app-sidebar__link:focus-visible,
:is(body[data-theme="midnight"], body[data-theme="default"]) .app-header__menu-toggle:focus-visible,
:is(body[data-theme="midnight"], body[data-theme="default"]) .site-nav__user:focus-visible,
:is(body[data-theme="midnight"], body[data-theme="default"]) .app-header__forum-nav-link:focus-visible,
:is(body[data-theme="midnight"], body[data-theme="default"]) select:focus-visible {
    outline-color: var(--rb-brand-green);
}

/* ============================================================================================
   Weather alert (blue, steady glow) + steel/amber control-palette addendum (2026-09-09, same day)
   -- extends the Midnight theme redesign above, from a second approved standalone HTML mockup
   (not part of this repo). Still entirely gated on body[data-theme="midnight"]; every rule below
   is exactly as inert on Default/Sunset/Forest/Custom as the rest of this file's Midnight section.
   The existing black background, semi-transparent posts, chrome-text titles, and red/green alert
   glow are all UNCHANGED by this addendum -- nothing below edits a prior rule, it only adds new
   ones. Two parts:
   (1) a third alert color, blue, for a calm "weather / clear-sky" system notice
       (Post.post_type == "WEATHER", accounts/models.py -- see that field's own comment and
       migrations/0078_post_weather_type.py) -- same glow treatment as CRASH_ALERT/system-notice,
       but STEADY, not pulsing: a calm/positive notice shouldn't visually compete for urgency with
       a pulsing emergency alert.
   (2) a complementary steel-blue/amber "control" palette for everything that ISN'T a headline
       alert -- secondary/cancel buttons, form-input borders/focus rings, inactive-tab text, and
       informational (non-alert) badges. --rb-brand-green (added by the nav/menu follow-up pass
       above) stays the one primary accent for links/primary actions and the nav/menu surfaces it
       already owns -- nothing below touches an active tab, a primary button, or any nav rule from
       that pass. Steel (--rb-steel, a cool muted blue-grey) was deliberately chosen to echo the
       chrome-text gradient's own metallic tone rather than compete with it. Amber (--rb-amber) is
       reserved for the one genuinely pending/caution semantic already in this app's UI (see
       .rider-list__state below) -- not force-fit onto anything that isn't actually that state.
   ============================================================================================ */

/* New tokens for this addendum only, added to the same body[data-theme="midnight"] scope the
   original pass's token-override block already established (a separate rule with the same
   selector, not an edit to that block's own text -- same "append, don't rewrite history"
   convention this file's nav/menu and chrome-text-widening passes both already followed). */
:is(body[data-theme="midnight"], body[data-theme="default"]) {
    --rb-blue: #2fa8ff;
    --rb-steel: #7c93b0;
    --rb-amber: #e8a33d;
}

/* Weather / clear-sky system notice -- third branch of static/js/timeline.js's buildPostCard()
   system-post detection, checked directly by post_type (like CRASH_ALERT), not via
   SYSTEM_AUTHOR_EMAIL (see that constant's own comment in timeline.js). Values are the approved
   mockup's exact numbers; the mockup's own selector was ".post--weather" against a generic
   "post" class this app doesn't use -- mapped onto this app's real .post-card--* modifier
   convention (.post-card--crash-alert/.post-card--system-notice), same as every other class in
   this section. Third gradient stop (rgba(236, 231, 224, 0.02)) is the same "regular post card"
   background-end color .post-card--crash-alert/.post-card--system-notice already end on above --
   not a new value, kept identical so all three alert tiers fade into the same base card tone. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .post-card--weather {
    border-color: rgba(47, 168, 255, 0.45);
    background: linear-gradient(180deg, rgba(47, 168, 255, 0.10), rgba(47, 168, 255, 0.025) 65%, rgba(236, 231, 224, 0.02));
    box-shadow:
        0 0 0 1px rgba(47, 168, 255, 0.20),
        0 0 26px 3px rgba(47, 168, 255, 0.22),
        0 0 60px 8px rgba(47, 168, 255, 0.08);
    /* Deliberately no `animation` property -- steady glow only, per the task's own explicit
       "does NOT pulse" requirement. Nothing to disable under prefers-reduced-motion either, since
       there was never any motion here to begin with -- this card was reduced-motion-safe from the
       moment it was written. */
}

/* --- Steel-blue control palette --------------------------------------------------------------
   Secondary/cancel buttons, form-input borders/focus rings, inactive-tab text, and informational
   (non-alert) badges. Every selector below targets an element that was already secondary/
   inactive/informational before this pass -- this never recolors a primary button, an active tab,
   or a primary/alert badge. */

/* Secondary/cancel buttons -- this codebase's own established "secondary/destructive action"
   classes (see .report-form__cancel's and .delete-btn's own comments near their definitions
   earlier in this file, and .new-thread-form__row button[type="button"]'s identical Cancel-button
   shape, reused verbatim by the Forum new-thread form, the Groups "Create Group" inline form, and
   moderation's action forms). All three swap from a plain muted/accent look to the steel accent;
   .btn-accent and every plain button[type="submit"] (the "positive/primary action" style, see that
   class's own comment) are deliberately untouched -- those stay on --rb-accent, matching "green
   stays the one primary brand accent for primary actions" for this pass's purposes. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .report-form__cancel,
:is(body[data-theme="midnight"], body[data-theme="default"]) .delete-btn,
:is(body[data-theme="midnight"], body[data-theme="default"]) .new-thread-form__row button[type="button"] {
    color: var(--rb-steel);
    border-color: color-mix(in srgb, var(--rb-steel) 45%, transparent);
}

/* Form-input borders -- every text-entry control sitewide (this file has no single shared input
   class; .settings-field/.search-form/.new-thread-form__field/.composer__field/etc. each declare
   their own near-identical border rule) swaps its neutral text-mix border for a steel tint. A
   plain type-selector list scoped under body[data-theme="midnight"] reaches every one of them at
   once without a per-page selector list -- (body + [data-theme] + input[type=...]) always
   out-specifies any single-class rule like ".settings-field input[type=...]" by element-count
   tiebreak once class/attribute counts are equal, confirmed by hand before relying on it here, so
   this wins regardless of which page's input rule declared the border first. */
:is(body[data-theme="midnight"], body[data-theme="default"]) input[type="text"],
:is(body[data-theme="midnight"], body[data-theme="default"]) input[type="number"],
:is(body[data-theme="midnight"], body[data-theme="default"]) input[type="date"],
:is(body[data-theme="midnight"], body[data-theme="default"]) input[type="email"],
:is(body[data-theme="midnight"], body[data-theme="default"]) input[type="password"],
:is(body[data-theme="midnight"], body[data-theme="default"]) input[type="search"],
:is(body[data-theme="midnight"], body[data-theme="default"]) input[type="color"],
:is(body[data-theme="midnight"], body[data-theme="default"]) textarea,
:is(body[data-theme="midnight"], body[data-theme="default"]) select {
    border-color: color-mix(in srgb, var(--rb-steel) 45%, transparent);
}

/* Focus rings on those same text-entry controls -- input/textarea only. <select> is deliberately
   excluded: the nav/menu follow-up pass above already put every <select>'s focus-visible ring on
   the brand green (bundled in with "every <select>" for the native dark-popup fix's own
   verification), and that's an already-shipped, already-verified behavior this addendum doesn't
   revisit. */
:is(body[data-theme="midnight"], body[data-theme="default"]) input:focus-visible,
:is(body[data-theme="midnight"], body[data-theme="default"]) textarea:focus-visible {
    outline-color: var(--rb-steel);
}

/* Inactive-tab text -- .forum-tab/.profile-tab (this codebase's two tab-bar components) and the
   Forum/Help header sub-nav's inactive links. :not() excludes each component's own --active
   modifier class explicitly, rather than relying on source order or specificity to leave it alone
   -- both classes coexist on the same element when a tab is active (confirmed in forum.js/
   profile.js/base.html before writing this), and a plain ".forum-tab { color: steel }" rule here
   would otherwise outrank ".forum-tab--active { color: accent }" by element-count and incorrectly
   steel-tint the active tab too. :hover (a background-only change) is unaffected either way. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .forum-tab:not(.forum-tab--active),
:is(body[data-theme="midnight"], body[data-theme="default"]) .profile-tab:not(.profile-tab--active),
:is(body[data-theme="midnight"], body[data-theme="default"]) .app-header__forum-nav-link:not(.app-header__forum-nav-link--active) {
    color: var(--rb-steel);
}

/* Informational (non-alert) badges -- .forum-category-badge (a thread's category pill),
   .state-badge/.mileage-badge (Profile's read-only lifetime-mileage/states-visited stats, see
   those classes' own "no live interaction, just a value" comment), .stat-card__badge (Rider
   Profile's equivalent read-only stat pill), and .app-sidebar__badge's base "Soon" labels
   (Marketplace/Help) -- every one of these is inert, read-only, non-interactive content, never an
   alert. :not() excludes .app-sidebar__badge--unread, which already got its own brand-green
   treatment (background + --rb-bg text, for contrast) in the nav/menu follow-up pass above and
   coexists as a second class on the same element (base.html) -- left untouched here. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .forum-category-badge,
:is(body[data-theme="midnight"], body[data-theme="default"]) .state-badge,
:is(body[data-theme="midnight"], body[data-theme="default"]) .mileage-badge,
:is(body[data-theme="midnight"], body[data-theme="default"]) .stat-card__badge,
:is(body[data-theme="midnight"], body[data-theme="default"]) .app-sidebar__badge:not(.app-sidebar__badge--unread) {
    color: var(--rb-steel);
    border-color: color-mix(in srgb, var(--rb-steel) 45%, transparent);
}

/* --- Amber: the one genuinely pending/caution state already in this app's UI -----------------
   .rider-list__state (no modifier) is the plain-text span search.js/groups.js/rider_profile.js
   all three render for the exact same semantic: an outstanding action still awaiting a response
   -- "Requested" (Search's own send-request flow, and Groups' join-request flow) and "Request
   Pending" (Rider Profile's mirror of the same friend-request state). Confirmed by reading all
   three call sites before writing this rule -- every non-error use of this class is this one
   pending semantic, nothing else. :not() excludes .rider-list__state--error (a different,
   unrelated "something went wrong" state that already renders in --rb-accent's red-ish fallback,
   see that modifier's own rule) -- both classes can appear on the same element, and without the
   :not() this selector's higher specificity (class+attribute vs. a single class) would otherwise
   incorrectly amber-tint the error state too, the same category of mistake the steel inactive-tab
   rule above deliberately guards against. */
:is(body[data-theme="midnight"], body[data-theme="default"]) .rider-list__state:not(.rider-list__state--error) {
    color: var(--rb-amber);
}

/* ==========================================================================
   Weather (added 2026-09-09) -- see CLAUDE.md's "Weather" section. Same --rb-*/color-mix() token
   approach as every other page in this file -- no hardcoded colors, so a logged-in rider's
   reflected theme still applies here. .weather-page itself is already listed in the shared
   bare-`main`-override selector above; only the forecast-card grid is new here.
   ========================================================================== */
.weather-page {
    gap: 1rem;
}

/* Weekday / Day weather / Night weather 3-column table -- mirrors the sibling mobile app's own
   redesigned WeatherScreen.kt layout exactly (see that repo's CLAUDE.md, "Weather screen UI
   redesign"): one row per calendar day, day always the middle column and night always the right
   column regardless of how NWS ordered the response (groupPeriodsByDay() in weather.js guarantees
   this), temperatures under each icon, no separate period-name text (the weekday column already
   gives that context). A missing day/night slot (a lone leading "Tonight" row, a lone trailing
   day-only row) renders an empty cell rather than an absent one, so every row's three columns
   stay aligned under the header. */
.weather-table-header {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    padding: 0 0.5rem;
}

.weather-table-header span {
    text-align: center;
    font-size: 0.85rem;
    color: var(--rb-label, #a39fae);
}

.weather-table-header span:first-child {
    visibility: hidden;
}

.weather-day-row {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 0.5rem;
    border-bottom: 1px solid color-mix(in srgb, var(--rb-text, #edeae3) 10%, transparent);
}

.weather-day-row:last-child {
    border-bottom: none;
}

.weather-day-row__weekday {
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--rb-text, #edeae3);
    margin: 0;
}

.weather-period-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.25rem;
    min-height: 64px;
    justify-content: center;
}

.weather-period-cell__icon {
    width: 56px;
    height: 56px;
}

.weather-period-cell__temp {
    font-size: 1rem;
    font-weight: 700;
    color: var(--rb-text, #edeae3);
    margin: 0;
}
