/* REDESIGN PHASE 8: gives anchor-link navigation (Settings' new sidebar,
   and the pre-existing Guide page's own identical sidebar) a smooth jump
   instead of an instant one. Harmless globally -- this only ever applies
   when something actually triggers an anchor scroll or scrollIntoView,
   neither of which happens anywhere else in this app. */
html { scroll-behavior: smooth; }

:root {
  /* .topbar keeps using --navy/--blue exactly as before, unchanged, at the
     user's explicit request while the rest of the dashboard gets a dark
     redesign one section at a time. */
  --navy: #1F3864;
  --blue: #2E75B6;
  --bg: #0B0B10;
  --card: #17171E;
  --card-alt: #1E1E27;
  --border: #2A2A35;
  --text: #F2F2F5;
  --muted: #8B8B99;
  --green: #2ECC71;
  --red: #EF4444;
  /* Tenth audit #2: same --X/--X-text split as --pink/--pink-text below, now
     applied to green/red -- --green/--red are each also used as a solid
     button background with white text (.bulk-actions button.bulk-freevend
     .active, .modal .actions button.danger, .bulk-actions
     button.bulk-offline.active); white-on-#2ECC71 is ~2.10:1 and
     white-on-#EF4444 is ~3.76:1, both well under 4.5:1. --green/--red
     already pass fine AS TEXT in both themes (confirmed: only the
     button-background case fails), so --green-text/--red-text simply equal
     --green/--red here -- this purely gives the text role its own tracked
     name, same reasoning as --paid-blue, so it can't accidentally regress
     if --green/--red (the button-background role) is ever changed later for
     some unrelated reason. --green/--red themselves stay exactly as they
     were everywhere they're a button background -- fixing THAT contrast
     case is a separate, harder problem, out of scope here (same call the
     ninth audit made for --pink/white). */
  --green-text: #2ECC71;
  --red-text: #EF4444;
  /* Seventeenth audit #2: --red-text itself stays exactly as it was above --
     it's correct and comfortably safe (~4.74:1) everywhere it's used against
     a genuinely SOLID --card (.error, .data-table td button.danger). This is
     a separate, purpose-specific variable for .badge.offline/
     .rp-incident-count only, whose real background is their own
     rgba(239,68,68,0.15) tint (see .badge.offline/.rp-incident-count below),
     not a solid color -- check-color-contrast.js used to approximate that
     pairing using --red-text against the solid --card alone, which produced
     a false PASS (~4.74:1) hiding a real WCAG AA failure once the tint is
     actually alpha-composited in (true contrast was only ~4.06:1, under the
     4.5:1 floor). Same "introduce a second, purpose-specific variable rather
     than compromise an already-safe one" pattern as --pink/--pink-text and
     --paid-blue/--paid-blue-bg above/below. Keeps the badge's own
     rgba(...) tint alpha completely unchanged (so its on-screen look doesn't
     change at all) -- only the TEXT color moves, tuned so the true
     composited contrast clears 4.5:1 with a real margin (verified ~5.0-5.5:1
     dark, ~5.0:1 light) instead of landing right back at a razor-thin pass. */
  --red-badge-text: #FA6060;
  --pink: #EC1F6A;
  --orange: #F5A623;
  /* Follow-up to ninth audit F5: --pink itself (#EC1F6A) is used as a solid
     button background with white text in several places (.login-card
     button, .panel-header button.primary, .sub-tab-btn.active, etc.) --
     white-on-#EC1F6A is ~4.22:1, already borderline. Using --pink AS TEXT
     against dark --card/--card-alt (.data-table tr.brand-header,
     .stat-tile .value.tone-sessions, .dash-header .eyebrow, .booths-grid
     .brand-header) also computes to ~3.9-4.2:1 -- under 4.5:1 either way.
     Brightening --pink to fix the text case would push white-on-pink-button
     well under 4.5:1 too (the two uses pull luminance in opposite
     directions -- there's no single value that satisfies both). Introduced
     a second, purpose-specific variable instead, same pattern as
     --paid-blue below: --pink stays exactly as it was (button backgrounds,
     the .rp-bar-fill gradient), --pink-text is only used at the four
     text-color call sites, and only needed a change in the DARK theme
     (light --pink already passes >=4.5:1 as text against every surface,
     confirmed, so --pink-text's light value is simply the same as --pink's). */
  --pink-text: #FF4D8D;
  /* Ninth audit #3/F1: the "Paid" mode's blue used to be three hardcoded
     #6EA8FA literals with no light-theme override at all (see the light
     media query below and .sched-info-banner/.mode-pill.Paid/
     .bulk-actions button.bulk-paid) -- this dark value is unchanged. */
  --paid-blue: #6EA8FA;
  /* Fifteenth audit #4: the fourteenth audit's own fix below (switching
     .calendar-block.Paid/.bulk-actions button.bulk-paid.active from the
     unthemed --blue to --paid-blue) reintroduced the exact --pink-shaped
     as-text-vs-as-button-background tension it should have avoided --
     --paid-blue's dark value is a light pastel chosen so it reads well AS
     TEXT against dark surfaces, which computes to only ~2.43:1 as a solid
     button BACKGROUND with white text (well under the 4.5:1 AA floor).
     Rather than accept this as a permanent trade-off (as the fourteenth
     audit did), a dedicated --paid-blue-bg is introduced here, exactly the
     same pattern already used for --calendar-freevend-bg just below: a
     background-ONLY variable, safe for white text in both themes, that
     never has to also serve as readable text against a dark surface. Its
     value reuses --paid-blue's own LIGHT-theme value (#1D5BB8, already
     verified elsewhere in this file at ~6.48:1 against white) since that's
     a known-safe, already-audited value in this exact family of blue --
     used unchanged in both themes here since (like
     --calendar-freevend-bg) this is only ever a solid background with
     white text, so the relevant contrast doesn't change between themes.
     --paid-blue itself is completely untouched everywhere it's still used
     as text (.mode-pill.Paid, .sched-info-banner, etc.). */
  --paid-blue-bg: #1D5BB8;
  /* Fourteenth audit #4: .calendar-block.Paid and .bulk-actions
     button.bulk-paid.active both still used the deliberately-unthemed
     --blue directly (a leftover -- .bulk-actions button.bulk-paid's own
     UNSELECTED state, one rule below it, already correctly used
     --paid-blue) instead of this variable -- in light mode this rendered
     the active Paid filter/calendar block in a different, dark-optimized
     blue than every other Paid-mode UI element, and was a complete blind
     spot for check-color-contrast.js (passed today only by accident, at
     ~4.52:1, barely over the 4.5:1 AA floor for white text, with no
     tracked pair protecting it). Both switched to --paid-blue like every
     sibling at the time -- since superseded by the fifteenth audit #4 fix
     immediately above, which moved these two rules' `background:` onto the
     new --paid-blue-bg instead (--paid-blue itself was never a safe
     button-background color in dark mode; see PAIRS/BUTTON_BG_PAIRS in
     check-color-contrast.js for the current tracked coverage). */
  /* Eleventh audit #4: the Schedule tab calendar's "today" column header
     used to set `style="color:var(--blue)"` inline from app.js, with no
     light/dark override -- --blue is deliberately unthemed (see the
     comment at the top of this block; only .topbar is meant to use it),
     so this computed to ~3.41:1 against .calendar-day-header's --card-alt
     background here in dark mode (and ~4.29:1 in light mode, see below),
     both under the WCAG AA 4.5:1 floor. Same fix pattern as --paid-blue: a
     dedicated variable, lightened relative to --blue so it actually reads
     against a dark --card-alt (verified ~5.59:1 here). */
  --calendar-today: #5B9BD5;
  /* Eleventh audit #5: .calendar-block.FreeVend's background used to be a
     bare hardcoded #A66A00 hex literal -- not a variable at all, so
     check-color-contrast.js's parseVars couldn't see it. White text on it
     computed to ~4.48:1, just under the 4.5:1 AA floor. Darkened slightly
     into this themed variable so it's both checkable and passes (verified
     ~6.85:1 against white here) while staying a recognizable warm
     amber/orange. Deliberately the SAME value in both this block and the
     light-media override below -- unlike --paid-blue/--calendar-today,
     this is only ever used as a solid background with white text, never
     as text against a themed surface, so the relevant contrast (white on
     this color) doesn't change between themes the way text-on-surface
     contrast does. */
  --calendar-freevend-bg: #805000;

  /* REDESIGN PHASE 0 -- new tokens from the design-token table (see
     REDESIGN_PLAN.md). Every existing token it names (--paid-blue, --pink,
     --pink-text, --orange, --red-badge-text, etc.) already exists above
     under its real name with its own measured contrast history -- reused
     as-is, not duplicated under the handoff's own prototype names.
     --faint's value is NOT the handoff's proposed #6B6B78: that value only
     clears WCAG's 3:1 "large text" floor against --card (3.396:1) and
     --today-col (3.292:1), never the 4.5:1 floor its own stated use
     ("calendar hour labels, separators") needs -- hour labels are small
     type, not large text, so 3:1 is the wrong bar. Lightened to #83838F,
     confirmed >=4.5:1 against --card (4.762), --bg (5.243) and
     --today-col (4.624) -- the exact "measure the real combination before
     shipping a text colour" rule this project's own contrast-regression
     history (four prior rounds, all in check-color-contrast.js's own
     comments) exists to enforce.
     These were ORIGINALLY added as a second, separate :root{} block lower
     in this file -- found and fixed before any screen used them: the
     contrast checker locates "the :root block" via `css.indexOf(':root')`,
     the FIRST match only, so a second :root block's contents were silently
     invisible to it. Merged in here instead, so they're checked by the
     same mechanism as every other dark-theme token in this file. */
  --card-hover: #25252F;
  --row-hover: #1B1B23;
  --border-soft: #22222B;
  --border-strong: #33333F;
  --border-hover: #4A4A57;
  --text-2: #B4B4C0;
  --faint: #83838F;
  --pink-hover: #D41A5E;
  --today-col: #1A1A22;
}
/* Light theme: overrides the same custom properties above (nothing else --
   every component already reads these vars, so a light palette falls out
   automatically with no structural changes needed). --navy/--blue are
   deliberately left unchanged: the topbar's actual look comes from its own
   hardcoded colors (#fff text, rgba(255,255,255,...) hover/border tints),
   not --bg/--text/--card, so it already reads fine against either theme
   without adjustment -- same reasoning the dark redesign used for keeping
   the topbar on its original navy palette. Accent colors used as text
   (--pink/--orange/--green/--red) are darkened slightly relative to their
   dark-theme values so they stay readable against a light background. */
@media (prefers-color-scheme: light) {
  :root {
    --bg: #F4F5F9;
    --card: #FFFFFF;
    --card-alt: #F0F1F6;
    --border: #D9DCE6;
    --text: #1A1B23;
    --muted: #62636F;
    /* Ninth audit #10: #1E9E5A against a white/near-white card computed to
       ~3.44:1 for the .bulk-actions button.bulk-freevend.active white-on-
       green combination -- below WCAG AA's 4.5:1 for normal text. Darkened
       to a value that clears 4.5:1 against every --card/--card-alt/--bg
       usage of --green in this theme (verified: ~5.0-7.1:1 across each),
       while staying recognizably green. */
    --green: #166534;
    --red: #DC2626;
    /* Tenth audit #2: --green-text/--red-text simply equal --green/--red in
       this theme too -- see the :root comment above for why. */
    --green-text: #166534;
    --red-text: #DC2626;
    /* Seventeenth audit #2: same purpose-specific split as the dark theme's
       :root block above -- --red-text is unchanged and stays safe (~4.83:1)
       for the genuinely-solid-surface cases (.error, .data-table td
       button.danger); this is only for .badge.offline/.rp-incident-count's
       own rgba(239,68,68,0.15) tinted background, whose true composited
       contrast at the old --red-text value was only ~3.97:1 -- a real,
       checker-confirmed AA failure once actually alpha-composited. Verified
       ~5.0:1 true composited contrast with this value. */
    --red-badge-text: #B92846;
    --pink: #C81B62;
    /* --pink-text: light --pink already passes >=4.5:1 as text against
       --card/--card-alt/--bg (verified: ~4.91-5.54:1) -- no separate value
       needed here, unlike the dark theme (see the :root comment above). */
    --pink-text: #C81B62;
    /* Follow-up to ninth audit F5: #B36B00 computed to ~3.71-4.18:1 against
       this theme's --card/--card-alt/--bg (below the 4.5:1 floor) for
       .mode-pill.FreeVend/.badge.pending/.sched-locked-mode/.stat-tile
       .value.tone-freevend text -- darkened further; verified ~5.66-6.39:1
       against all three surfaces, comfortably clearing AA while staying a
       recognizable amber/orange. --orange is never used as a button
       background with white text (only as its own tinted-translucent
       background, e.g. .badge.pending), so no white-on-orange tension like
       --pink's applies here -- a single darkened value is sufficient. */
    --orange: #8A5200;
    /* Ninth audit #3/F1: darkened from the dark theme's #6EA8FA so
       .sched-info-banner / .mode-pill.Paid / .bulk-actions button.bulk-paid
       text is actually readable against this theme's light --card/
       --card-alt/--bg surfaces (verified: ~5.7-6.5:1, comfortably above
       the 4.5:1 WCAG AA floor for normal text) -- the previous dark-
       optimized value computed to ~1.7-1.9:1 here. */
    --paid-blue: #1D5BB8;
    /* Fifteenth audit #4: same value as --paid-blue right above in this
       theme (both happen to already be #1D5BB8) -- see the :root comment
       above for why this is deliberately the SAME value in both themes
       (only ever a solid background with white text, so unlike --paid-blue
       itself, its required contrast doesn't change between themes). */
    --paid-blue-bg: #1D5BB8;
    /* Eleventh audit #4: darkened from the dark theme's #5B9BD5 (itself
       already darker than plain --blue) so the calendar "today" header
       text is actually readable against this theme's light --card-alt
       (verified ~7.32:1, comfortably above the 4.5:1 AA floor) -- the
       previous --blue value computed to only ~4.29:1 here. */
    --calendar-today: #1B4F8C;

    /* REDESIGN PHASE 0 -- same tokens as the dark :root block above. --faint
       here is #6D6E78, NOT the handoff's #8A8B96: that value clears only
       3.1-3.4:1 against this theme's real surfaces (--card/--bg/--today-col
       below), never the 4.5:1 floor its stated small-type use needs.
       #6D6E78 confirmed >=4.5:1 against --card (5.058), --bg (4.643) and
       --today-col (4.546, the tightest of the three). */
    --card-hover: #E8EAF0;
    --row-hover: #F7F8FB;
    --border-soft: #E4E6EE;
    --border-strong: #CDD1DC;
    --border-hover: #B5BAC8;
    --text-2: #3F404B;
    --faint: #6D6E78;
    /* Not given a separate light value by the handoff (its light table
       lists no --pink-hover row) -- --pink itself needs no light/dark
       split (same reasoning already documented at --pink's own
       declaration: it's always a solid background with white text, so the
       required contrast doesn't change between themes), so its hover
       state doesn't either. Kept identical to the dark value. */
    --pink-hover: #D41A5E;
    --today-col: #FBF0F4;
  }
}
/* Ninth audit F4: manual light/dark theme override, set via a
   data-theme="light"/"dark" attribute on <html> by the topbar toggle in
   app.js. :root[data-theme="..."] has higher specificity (0,2,0) than a
   bare :root (0,1,0), including one wrapped in the prefers-color-scheme
   media query above, so an explicit choice always wins over the OS-driven
   default regardless of source order -- placed after the media query here
   purely for readability. When no data-theme attribute is set at all (the
   default "System" choice), only the media query above applies, exactly as
   before this feature existed. Values mirror the :root/light-media blocks
   above exactly, so all three theme paths (system-dark, system-light,
   forced-dark, forced-light) always agree. */
:root[data-theme="dark"] {
  --bg: #0B0B10;
  --card: #17171E;
  --card-alt: #1E1E27;
  --border: #2A2A35;
  --text: #F2F2F5;
  --muted: #8B8B99;
  --green: #2ECC71;
  --red: #EF4444;
  --green-text: #2ECC71;
  --red-text: #EF4444;
  /* Seventeenth audit #2: mirrors the dark :root block's own --red-badge-text
     (see its comment there for the full reasoning) exactly, same as every
     other variable in this data-theme override block. */
  --red-badge-text: #FA6060;
  --pink: #EC1F6A;
  --pink-text: #FF4D8D;
  --orange: #F5A623;
  --paid-blue: #6EA8FA;
  --paid-blue-bg: #1D5BB8;
  --calendar-today: #5B9BD5;
  /* REDESIGN PHASE 0 -- mirrors the dark :root block's own tokens exactly
     (see its comment there for the full reasoning and measured ratios). */
  --card-hover: #25252F;
  --row-hover: #1B1B23;
  --border-soft: #22222B;
  --border-strong: #33333F;
  --border-hover: #4A4A57;
  --text-2: #B4B4C0;
  --faint: #83838F;
  --pink-hover: #D41A5E;
  --today-col: #1A1A22;
}
:root[data-theme="light"] {
  --bg: #F4F5F9;
  --card: #FFFFFF;
  --card-alt: #F0F1F6;
  --border: #D9DCE6;
  --text: #1A1B23;
  --muted: #62636F;
  --green: #166534;
  --red: #DC2626;
  --green-text: #166534;
  --red-text: #DC2626;
  /* Seventeenth audit #2: mirrors the light-media block's own
     --red-badge-text (see its comment there for the full reasoning)
     exactly, same as every other variable in this data-theme override
     block. */
  --red-badge-text: #B92846;
  --pink: #C81B62;
  --pink-text: #C81B62;
  --orange: #8A5200;
  --paid-blue: #1D5BB8;
  --paid-blue-bg: #1D5BB8;
  --calendar-today: #1B4F8C;
  /* REDESIGN PHASE 0 -- mirrors the light-media block's own tokens exactly
     (see its comment there for the full reasoning and measured ratios). */
  --card-hover: #E8EAF0;
  --row-hover: #F7F8FB;
  --border-soft: #E4E6EE;
  --border-strong: #CDD1DC;
  --border-hover: #B5BAC8;
  --text-2: #3F404B;
  --faint: #6D6E78;
  --pink-hover: #D41A5E;
  --today-col: #FBF0F4;
}
* { box-sizing: border-box; }
body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: var(--bg); color: var(--text); }
.hidden { display: none !important; }
.muted { color: var(--muted); }

/* round-44 frontend audit: until now this file had ZERO :disabled styling
   of any kind, so a disabled button was pixel-identical to an enabled one.
   Every button rule below sets its own background AND color, which
   overrides the UA stylesheet's own greying-out entirely, and each also
   sets cursor:pointer -- so nothing about a disabled button looked or felt
   disabled. Two real cases: boothCard() renders Reboot disabled while
   rebootRequestedAt is set and Reset-camera-USB disabled while
   usbResetRequestedAt is set, and withButtonBusy disables a save button
   for the life of its request. Clicking a disabled button fires no event
   at all -- no confirm(), no flashActionBusy(), nothing -- so it reads as
   a broken button rather than a busy one.

   !important, and placed here beside .hidden rather than at the end of the
   file, for the same reason .hidden uses it: this is a global STATE that
   must beat component styling. Plain `button:disabled` is specificity
   (0,1,1), which loses to `.booth-card .booth-actions button` (0,2,1) --
   i.e. exactly the Reboot/Reset-camera-USB buttons this fix is most for.
   Importance beats specificity and source order outright, so this works
   from the utility block where it semantically belongs and cannot be
   silently defeated by a future, more specific button rule.

   2026-10-02 audit, finding 15: this used to be scoped to `button` only,
   with a comment asserting that the only other disabled control in the app
   was the Download-agent-installer <a>. That stopped being true the moment
   the Edit-login modal shipped: it renders <select id="m-useredit-role">
   disabled when you are editing your OWN account, and a disabled <select>
   here was pixel-identical to an interactive one -- so the one control the
   modal deliberately takes away from you looked exactly like the two beside
   it that still worked, with only the prose underneath to say otherwise.

   Broadened to every form-control element rather than adding `select` and
   waiting for the next one. That is this codebase's own recurring lesson
   (see check-form-element-styling.js and the textarea gap it was written
   for): a rule that enumerates the element types that existed when it was
   written gives the next one nothing. <a> still cannot match :disabled, so
   the Download-agent-installer link's own aria-disabled + inline opacity
   remains the right mechanism there and these still cannot fight.

   No contrast entry is needed for this: WCAG 1.4.3 explicitly exempts text
   that is part of an inactive user interface component, and the checker
   compares CSS-variable pairs and is unaffected by opacity regardless. */
button:disabled,
select:disabled,
input:disabled,
textarea:disabled { opacity: 0.55 !important; cursor: not-allowed !important; }

/* The per-venue weekly alerting schedule (Manage venues -> Edit). Seven
   rows of two <input type="time">, so it wants to be compact rather than
   inheriting .data-table's row padding -- this sits inside a modal that
   already carries a recipients field, a seven-way type picker and two
   paragraphs of explanation. */
.alert-schedule { width: 100%; border-collapse: collapse; margin-top: 6px; }
.alert-schedule th {
  text-align: left; font-size: 12px; font-weight: 600;
  color: var(--muted); padding: 2px 6px 4px 0;
}
.alert-schedule td { padding: 2px 6px 2px 0; }
.alert-schedule td:first-child { font-size: 13px; white-space: nowrap; }
/* Overrides the modal's own `label input { display:block; width:100% }`:
   these two sit side by side in a row, not stacked under a label. */
.alert-schedule input[type="time"] { width: 100%; margin: 0; }

/* Twenty-ninth audit #2: height:100vh + align-items:center with no
   overflow-y meant an oversized card (a short viewport -- phone in
   landscape, or significant text-zoom) got centered by pushing its TOP
   above the document's own scrollable area (y can't go negative), making
   the heading/email field genuinely unreachable rather than just needing
   a scroll. min-height (not height) + overflow-y:auto + some vertical
   padding lets the whole screen scroll instead of clipping upward. */
.login-screen { display: flex; align-items: center; justify-content: center; min-height: 100vh; overflow-y: auto; padding: 24px 0; }
/* Twenty-eighth audit #2: the login screen was the one screen in this app
   with no responsive handling at all -- a fixed width and no max-width
   meant it could overflow horizontally on any viewport narrower than 340px
   (still-shipping small phones, or a zoomed/large-text mobile browser),
   unlike every other screen this project's mobile pass already covers. */
.login-card { background: var(--card); border: 1px solid var(--border); padding: 40px; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); width: 340px; max-width: calc(100vw - 32px); }
.login-card h1 { font-size: 20px; color: var(--text); margin: 0 0 6px; }
.login-card label { display: block; margin-top: 16px; font-size: 13px; color: var(--muted); }
.login-card input { width: 100%; padding: 9px 10px; margin-top: 4px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; background: var(--card-alt); color: var(--text); }
.login-card button { margin-top: 22px; width: 100%; padding: 10px; background: var(--pink); color: #fff; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; }
.error { color: var(--red-text); font-size: 13px; min-height: 18px; }

.topbar { display: flex; align-items: center; justify-content: space-between; background: var(--navy); color: #fff; padding: 0 20px; height: 56px; }
.brand { font-weight: bold; cursor: pointer; }
.tabs { display: flex; gap: 4px; }
.tab-btn { background: transparent; border: none; color: #cfd8e3; padding: 10px 14px; cursor: pointer; font-size: 14px; border-radius: 6px; }
.tab-btn.active, .tab-btn:hover { background: rgba(255,255,255,0.12); color: #fff; }
/* Twenty-first audit #2: the resolved-theme label ("(currently dark)") next
   to the theme-toggle button sits directly in .topbar, which -- like
   .tab-btn above -- is deliberately, permanently unthemed. The themed
   .muted class (var(--muted)) failed WCAG AA against --navy in both
   resolutions (~3.46:1 dark, ~1.95:1 light); reuses .tab-btn's own already-
   audited #cfd8e3 (already allowlisted in test/check-color-contrast.js's
   ALLOWLISTED_HEX_LITERALS for exactly this "permanently-unthemed topbar"
   reason) rather than introducing a new, separately-audited value. */
.topbar-muted { color: #cfd8e3; }
.who { display: flex; align-items: center; gap: 10px; font-size: 13px; }
.who button { background: transparent; border: 1px solid rgba(255,255,255,0.4); color: #fff; padding: 6px 10px; border-radius: 6px; cursor: pointer; }

main { max-width: 1200px; margin: 24px auto; padding: 0 20px; }
.panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.panel-header h2 { color: var(--text); margin: 0; }
.panel-header select { padding: 7px; border-radius: 6px; border: 1px solid var(--border); margin-right: 8px; background: var(--card-alt); color: var(--text); }
button { font-family: inherit; }
.panel-header button, .modal button.primary, .settings-form button.primary { background: var(--pink); color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; }
.panel-header button.view-toggle { background: var(--card-alt); color: var(--text); border: 1px solid var(--border); margin-right: 4px; }
.panel-header button.view-toggle.active { background: var(--pink); color: #fff; border-color: var(--pink); }
/* Thirty-fifth audit round (finding #14): .panel-header button above matches
   ANY button inside a .panel-header by element alone, so the Schedule
   calendar's Prev/Today/Next buttons -- explicitly marked class="secondary"
   in index.html -- were still getting the pink primary look, the same
   specificity-collision shape .view-toggle above already had to work around.
   button.secondary here (0,2,1) beats the bare .panel-header button rule
   above (0,1,1) regardless of source order. */
.panel-header button.secondary { background: var(--card-alt); color: var(--text); border: 1px solid var(--border); }

/* Twenty-eighth audit #3: the calendar grid had no responsive handling at
   all -- unlike .data-table (display:block; overflow-x:auto for exactly
   this "too many columns" shape elsewhere in this file), its fixed 8-column
   layout (50px + 7 equal day columns) never shrank OR scrolled on a narrow
   viewport; any content that couldn't fit was silently clipped instead of
   reachable. Fixed by giving the grid a min-width and letting a wrapper
   scroll horizontally -- overflow:hidden stays on .calendar-grid itself (it
   clips its own children to the rounded corners below).
   Twenty-ninth audit #3: that scroll wrapper was originally .calendar
   itself (#schedule-calendar-view), which also contains the Prev/Today/Next
   nav row (.panel-header) as a SIBLING of the grid -- scrolling right to see
   later day columns scrolled the nav buttons out of view too. Moved the
   scroll behavior onto a new, dedicated .calendar-grid-scroll wrapper
   (index.html) around ONLY the grid, so .panel-header stays outside the
   scrolling region and navigation is always reachable regardless of scroll
   position. */
.calendar-grid-scroll { overflow-x: auto; }
/* Round-46 #17: wrapper for the two Reports tables that have no focusable
   descendants of their own. At <=820px .data-table becomes its own
   horizontal scroll container, so without this a keyboard-only user could
   not reach the right-hand columns. Carries role/tabindex/aria-label in the
   markup, matching F3 and the four .modal-scroll siblings. */
.table-scroll { overflow-x: auto; }
/* Round-46 #18: the calendar's scroll wrapper needs its own keyboard path for
   a week with NO slots. Calendar blocks carry tabindex="0" (round 28 #4), so a
   populated week is reachable indirectly -- an empty one had no focusable
   descendant at all, leaving the later day columns unreachable on a narrow
   viewport. Low value on its own (an empty week shows only day names) but it
   is the same class, and it would otherwise be rediscovered. */
.calendar-grid-scroll:focus-visible { outline: 2px solid var(--pink); outline-offset: 2px; }
.calendar-grid { display: grid; grid-template-columns: 50px repeat(7, 1fr); min-width: 600px; background: var(--card); border-radius: 8px; border: 1px solid var(--border); overflow: hidden; }
.calendar-day-header { text-align: center; font-size: 12px; font-weight: bold; color: var(--text); padding: 8px 0; background: var(--card-alt); text-transform: uppercase; }
.calendar-hours-track, .calendar-day-track { position: relative; height: 600px; }
.calendar-hour-label { position: absolute; font-size: 11px; color: var(--muted); padding-left: 4px; }
.calendar-day { border-left: 1px solid var(--border); }
.calendar-block { position: absolute; left: 2px; right: 2px; border-radius: 4px; font-size: 10px; line-height: 1.3; color: #fff; padding: 2px 4px; overflow: hidden; }
.calendar-block.Paid { background: var(--paid-blue-bg); }
.calendar-block.FreeVend { background: var(--calendar-freevend-bg); }
/* Fortieth audit #2: a small "+N" badge on any calendar block that's
   actually standing in for a GROUP of mutually-overlapping slots (see
   renderScheduleCalendar's own grouping logic in app.js) -- without this,
   a genuinely overlapping second/third slot was rendered completely hidden
   behind another full-width block with zero visible indication it existed.
   Reuses --card/--text (already-audited pairs elsewhere in this file)
   rather than introducing a new color. */
.calendar-block-overlap-badge { position: absolute; top: 1px; right: 1px; background: var(--card); color: var(--text); border-radius: 8px; font-size: 9px; font-weight: bold; line-height: 1.5; padding: 0 4px; }

/* REDESIGN PHASE 5: Reports header, same shape as .venues-header/
   .logos-header -- title+subtitle on the left, the (previously separate
   .rp-toolbar) range/venue/export controls now on the right instead of
   their own row below. .rp-toolbar itself is now dead (Phase 10 sweeps
   it, same as every other now-unreferenced class from each prior phase --
   this project's own deliberate "additive during the phase, delete only
   once the replacement is live, final cross-cutting sweep at the end"
   rule, not an oversight). */
.reports-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin: 14px 0 20px; }
.reports-header h1 { margin: 0 0 6px; font-size: 26px; font-weight: 600; letter-spacing: -0.02em; color: var(--text); }
.reports-header-right { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.reports-header-right select { background: var(--card); border: 1px solid var(--border); border-radius: 8px; color: var(--text); padding: 9px 12px; font-size: 13px; }

/* REDESIGN PHASE 6: Schedule header, the same .venues-header/.logos-header/
   .reports-header shape -- title+subtitle on the left, a venue filter + the
   List/Week toggle (now a .segmented pair, same component as the booth
   card's own Mode toggle and the override drawer's duration presets,
   rather than the old .panel-header/.view-toggle pink-by-default pair) +
   the primary "New slot" action on the right. */
.schedule-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin: 14px 0 20px; }
.schedule-header h1 { margin: 0 0 6px; font-size: 26px; font-weight: 600; letter-spacing: -0.02em; color: var(--text); }
.schedule-header .subtitle { max-width: 640px; }
.schedule-header-right { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.schedule-header-right select { background: var(--card); border: 1px solid var(--border); border-radius: 8px; color: var(--text); padding: 9px 12px; font-size: 13px; }
.schedule-show-past { font-size: 13px; color: var(--muted); display: block; margin-bottom: 8px; }
.schedule-show-past input { width: auto; vertical-align: middle; }
/* The calendar's own internal nav row (Prev/This/Next week + the visible
   date range) moves to the left so the new legend sits on the right in the
   same .panel-header flex row, instead of the range label and nav buttons
   occupying the row's two ends with nowhere left for a legend. */
.schedule-cal-nav-left { display: flex; align-items: center; gap: 8px; }
.schedule-cal-range-label { font-size: 15px; font-weight: 600; margin-left: 6px; color: var(--text); }
.schedule-cal-arrow { width: 34px; height: 34px; padding: 0; }
.schedule-legend { display: flex; align-items: center; gap: 16px; font-size: 12px; color: var(--text-2); }
.schedule-legend-item { display: flex; align-items: center; gap: 6px; }
.schedule-legend-swatch { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
/* Decorative swatches only (no text ever renders on top of them), so these
   are plain --orange/--paid-blue backgrounds -- the same two colors
   .mode-pill.FreeVend/.mode-pill.Paid already use as TEXT, reused here in
   their solid form purely as a color key. Nothing new for
   check-color-contrast.js to track: that checker exists for text-on-
   background pairs, and a swatch with no text inside it is neither. */
.schedule-legend-swatch.FreeVend { background: var(--orange); }
.schedule-legend-swatch.Paid { background: var(--paid-blue); }

/* REDESIGN PHASE 7: Users header, the same *-header/*-header-right shape as
   every prior phase -- title+subtitle on the left, the People/Login-
   activity .segmented toggle + Add login/Invite user buttons on the right. */
.users-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin: 14px 0 20px; }
.users-header h1 { margin: 0 0 6px; font-size: 26px; font-weight: 600; letter-spacing: -0.02em; color: var(--text); }
.users-header .subtitle { max-width: 640px; }
.users-header-right { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* A 32px initials circle in front of the stacked name/email, matching the
   handoff -- plain --text on --card-alt, the same baseline pairing
   .data-table cells already use everywhere, so nothing new for
   check-color-contrast.js to track. */
.user-avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--card-alt); border: 1px solid var(--border); display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; color: var(--text); flex-shrink: 0; }
.user-name-cell { display: flex; align-items: center; gap: 12px; min-width: 0; }
.user-name-cell .user-name-stack { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.user-name-cell .user-name-stack .user-email { font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.user-status-meta { font-size: 11px; color: var(--muted); }

/* A compact toggle switch for the Permissions matrix -- same track+knob
   visual language as the handoff's own flat permission-switch list, scaled
   down to fit a dense venue/booth x capability table cell rather than a
   single-column list. Purely decorative chrome (a plain white knob on a
   solid track, no text ever inside it), so nothing new for
   check-color-contrast.js to track, the same reasoning as the Schedule
   legend swatches above. */
.perm-switch { position: relative; display: inline-block; width: 30px; height: 17px; border-radius: 9px; border: none; background: var(--border); cursor: pointer; padding: 0; vertical-align: middle; }
.perm-switch.on { background: var(--pink); }
.perm-switch .perm-switch-knob { position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: #fff; transition: left 0.1s ease; }
.perm-switch.on .perm-switch-knob { left: 15px; }
/* The admin-locked state (every switch shown on, disabled, at 0.5 opacity,
   with the explanatory text above it) -- the handoff's own specified
   affordance for an admin's Permissions tab, applied here to the real
   venue/booth matrix rather than the handoff's simplified flat list (see
   openUserEditDrawer's own comment in app.js for why). */
.perm-switch:disabled { cursor: default; }
.user-perms-locked { opacity: 0.5; }

/* REDESIGN PHASE 8: Settings. Same title+subtitle header shape as every
   prior phase. The sidebar nav reuses .guide-nav as-is (defined below,
   built for the Guide page's own identical sticky-anchor-link-list shape)
   rather than a near-duplicate new class -- only the overall two-column
   flex layout and the card column are genuinely new. scroll-behavior:
   smooth (added once, on html, further down this file) gives both this
   page's and the pre-existing Guide page's anchor links the same smooth
   jump, which the Guide page never had before -- a harmless side benefit
   of adding the one declaration here, not a separate deliberate change to
   Guide. */
.settings-header { margin: 14px 0 20px; }
.settings-header h1 { margin: 0 0 6px; font-size: 26px; font-weight: 600; letter-spacing: -0.02em; color: var(--text); }
.settings-layout { display: flex; gap: 32px; align-items: flex-start; flex-wrap: wrap; }
.settings-main { flex: 1 1 560px; min-width: 0; display: flex; flex-direction: column; gap: 20px; }
.settings-main .ov-card { margin-bottom: 0; }

.rp-range { display: flex; gap: 3px; background: var(--card); border: 1px solid var(--border); border-radius: 9px; padding: 3px; }
.rp-range-btn { background: transparent; border: none; color: var(--muted); padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; }
.rp-range-btn.active { background: var(--card-alt); color: var(--text); font-weight: 500; }
.rp-source { font-size: 11px; }
.rp-source.real { color: var(--green-text); }

.rp-bars { padding: 2px; }
.rp-bar-row { display: grid; grid-template-columns: 120px minmax(0,1fr) 150px; align-items: center; gap: 16px; margin-bottom: 14px; font-size: 13px; }
.rp-bar-row:last-child { margin-bottom: 0; }
.rp-bar-label { color: var(--text); font-weight: 500; }
.rp-bar-track { background: var(--bg); border-radius: 5px; height: 10px; overflow: hidden; }
.rp-bar-fill { background: var(--orange); height: 100%; border-radius: 5px; }
.rp-bar-value { text-align: right; color: var(--text-2); font-variant-numeric: tabular-nums; font-family: 'Geist Mono', monospace; font-size: 12px; }

.rp-grid-2 { display: grid; grid-template-columns: 1.3fr 1fr; gap: 20px; align-items: start; }

/* By-TYPE hardware reliability cards (Camera/Printer/MDB), each showing a
   period total and which real booth had the most of that type -- replaces
   the prior by-booth list. Computed client-side from the same real
   hardwareIncidentsByBooth the server already sent (no new backend field);
   see renderHardwareIncidentCards' own comment in app.js for why the
   aggregate total is kept even though only the single worst booth per
   type is named. */
.rp-incident-type-card { display: flex; align-items: center; gap: 14px; padding: 12px 14px; background: var(--bg); border: 1px solid var(--border-soft); border-radius: 10px; margin-bottom: 8px; }
.rp-incident-type-card:last-child { margin-bottom: 0; }
.rp-incident-type-count { font-family: 'Geist Mono', monospace; font-size: 22px; font-weight: 500; width: 40px; flex-shrink: 0; }
.rp-incident-type-label { font-weight: 500; color: var(--text); }
.rp-incident-type-worst { font-size: 12px; color: var(--muted); }

.rp-donut-wrap { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.rp-donut { width: 120px; height: 120px; border-radius: 50%; position: relative; flex-shrink: 0; }
.rp-donut::after { content: ''; position: absolute; inset: 18px; background: var(--card); border-radius: 50%; }
.rp-legend { font-size: 13px; }
.rp-legend-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; color: var(--text); }
.rp-legend-row:last-child { margin-bottom: 0; }
.rp-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.rp-legend-sub { color: var(--muted); font-size: 12px; margin-left: 18px; }

.data-table { width: 100%; border-collapse: collapse; background: var(--card); border-radius: 8px; overflow: hidden; border: 1px solid var(--border); }
.data-table th { text-align: left; background: var(--card-alt); color: var(--muted); font-size: 12px; text-transform: uppercase; padding: 10px 12px; }
.data-table td { padding: 10px 12px; border-top: 1px solid var(--border); font-size: 14px; color: var(--text); }
.data-table td button { margin-right: 6px; background: var(--card-alt); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 5px 9px; cursor: pointer; font-size: 12px; }
/* Thirty-fourth audit round: color was --red-text, checked (incorrectly)
   against --card by check-color-contrast.js -- the real inherited
   background (from .data-table td button just above) is --card-alt, and
   --red-text fails AA against it (~4.28-4.39:1, both under 4.5:1). Switched
   to --red-badge-text (already tuned for exactly this near-black/near-white
   solid-surface case elsewhere in this file) instead of inventing a new
   variable. See check-color-contrast.js's own updated PAIRS entry. */
.data-table td button.danger { color: var(--red-badge-text); border-color: var(--red); }
.data-table tr.brand-header td { background: var(--card-alt); color: var(--pink-text); font-weight: bold; font-size: 12px; text-transform: uppercase; letter-spacing: 0.03em; padding: 8px 12px; }

.badge { display: inline-block; padding: 3px 9px; border-radius: 20px; font-size: 12px; font-weight: bold; }
.badge.online { background: rgba(46,204,113,0.15); color: var(--green-text); }
/* Seventeenth audit #2: color uses --red-badge-text, not --red-text -- see
   that variable's own comment in the :root block above for why. */
.badge.offline { background: rgba(239,68,68,0.15); color: var(--red-badge-text); }
.badge.pending { background: rgba(245,166,35,0.15); color: var(--orange); margin-left: 6px; }
.mode-pill { padding: 3px 9px; border-radius: 20px; font-size: 12px; font-weight: bold; }
.mode-pill.Paid { background: rgba(59,130,246,0.15); color: var(--paid-blue); }
.mode-pill.FreeVend { background: rgba(245,166,35,0.15); color: var(--orange); }

/* REDESIGN PHASE 4: Logos page header, mirroring Venues' own .venues-header
   (same flex/title/subtitle/action-row shape as every other ported page
   header in this redesign). */
.logos-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin: 14px 0 20px; }
.logos-header h1 { margin: 0 0 6px; font-size: 26px; font-weight: 600; letter-spacing: -0.02em; color: var(--text); }
.logos-header .subtitle { max-width: 620px; }
.logos-header-right { display: flex; gap: 8px; align-items: center; }
.logos-header-right input[type="search"] { background: var(--card); border: 1px solid var(--border); border-radius: 8px; color: var(--text); padding: 9px 12px; font-size: 13px; width: 180px; }

.logos-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }

/* The dashed "Drop a logo or browse" ghost tile, always the grid's own
   first child -- a second, equally-real trigger for the exact same upload
   flow as the header's "Upload logo" button, not a separate mechanism. */
.logo-upload-tile { background: transparent; border: 1.5px dashed var(--border-strong); border-radius: 14px; min-height: 220px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; color: var(--muted); cursor: pointer; padding: 20px; text-align: center; }
.logo-upload-tile:hover { border-color: var(--pink); color: var(--text); }
.logo-upload-tile .logo-upload-plus { width: 40px; height: 40px; border-radius: 10px; background: var(--card); border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; font-size: 20px; color: var(--text); }
.logo-upload-tile .logo-upload-title { font-size: 14px; font-weight: 500; color: var(--text); }
.logo-upload-tile .logo-upload-hint { font-size: 12px; line-height: 1.5; }

/* A real design requirement, not decorative (see REDESIGN_PLAN.md's own
   Phase 4 gate): logos are shown in Remote Pro's white logo slot on the
   booth screen, so the preview here is deliberately a SOLID WHITE box
   regardless of theme -- a dark-background logo asset would otherwise look
   fine in this card and wrong on every real booth. */
.logo-card { background: var(--card); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; display: flex; flex-direction: column; }
.logo-card-preview { height: 140px; background: #FFFFFF; display: flex; align-items: center; justify-content: center; padding: 20px; box-sizing: border-box; }
.logo-card-preview img { max-width: 100%; max-height: 100%; object-fit: contain; }
.logo-card-body { padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.logo-card-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
.logo-card-name { font-weight: 600; color: var(--text); min-width: 0; word-break: break-word; }
.logo-card-filemeta { font-family: 'Geist Mono', monospace; font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; margin-top: 2px; }
.logo-card-footer { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.logo-card-usage { font-size: 12px; color: var(--muted); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Thirty-fourth audit round: the Logos grid's "Delete" button had no
   styling at all -- no rule anywhere in this file matched a bare
   `button.danger` sitting directly inside .logo-card, so it rendered as
   the browser's native, unthemed button chrome, the one visibly
   inconsistent control in an otherwise fully dark-themed app. Mirrors
   .booth-card .booth-actions button's own identical "solid --card-alt
   chip on a --card-background card" pattern exactly, including reusing
   --red-badge-text for the danger variant (same real --card-alt
   ancestor as that fix, see .data-table td button.danger's own comment).
   Still true after the Phase 4 restyle -- button rules kept, scope
   narrowed from the old flat .logo-card button to .logo-card-footer
   button since the button no longer sits directly in .logo-card itself. */
.logo-card-footer button { flex-shrink: 0; background: var(--card-alt); color: var(--text); border: 1px solid var(--border); border-radius: 7px; padding: 5px 10px; cursor: pointer; font-size: 12px; }
.logo-card-footer button.danger { color: var(--red-badge-text); border-color: var(--red); }

/* REDESIGN PHASE 0: z-index added (previously none -- it relied entirely on
   DOM order, which was fine while it was the only fixed-position overlay in
   the app). Now that .drawer-backdrop (z-index 400) can be open at the same
   time -- a confirmation modal triggered from inside a drawer, e.g. the
   booth-settings drawer's own Delete flow -- the modal must stack above it
   regardless of where either element sits in index.html's source order, not
   by accident of document position. */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 450; }
.modal { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 24px; width: 380px; max-width: 90vw; max-height: 85vh; display: flex; flex-direction: column; }
.modal.wide { width: 520px; }
/* Permissions matrix: eight columns (venue/booth + seven capabilities).
   .wide's 520px forced horizontal scrolling, which for a grid of checkboxes
   is worse than useless -- you cannot see which column you are ticking.
   max-width keeps it inside the viewport on a laptop, and the 820px
   responsive block below still lets it scroll on a phone, where no width
   would fit eight columns. */
.modal.xwide { width: 1020px; max-width: 96vw; }
/* The checkbox columns need only enough room for the header word. Default
   .data-table padding (10px 12px) on eight columns wastes ~130px of width
   that the venue names actually need. */

/* .xwide is WIDTH ONLY, so any modal that needs room can use it. The
   grid-specific rules below live on their own class -- they were originally
   folded into .xwide, which would have squashed the columns of any other
   wide modal that happened to contain a table. */
.modal.matrix .data-table th, .modal.matrix .data-table td { padding: 7px 8px; }
.modal.matrix .data-table th:not(:first-child),
.modal.matrix .data-table td:not(:first-child) { width: 1%; }
.modal.matrix .data-table th { white-space: nowrap; }
.modal.matrix .data-table td:first-child { white-space: nowrap; }
.modal.matrix .modal-scroll { overflow-x: auto; }
.modal-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.modal h3 { margin-top: 0; color: var(--text); }
.modal label { display: block; margin-top: 12px; font-size: 13px; color: var(--muted); }
.modal input, .modal select { width: 100%; padding: 8px; margin-top: 4px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; background: var(--card-alt); color: var(--text); }
.modal input[type="checkbox"] { width: auto; padding: 0; margin: 0 6px 0 0; border: none; vertical-align: middle; }
.modal .actions { margin-top: 20px; display: flex; justify-content: flex-end; gap: 8px; }
.modal .actions button.secondary, .modal .actions a.secondary { background: var(--card-alt); color: var(--text); border: 1px solid var(--border); padding: 8px 14px; border-radius: 6px; cursor: pointer; display: inline-block; text-decoration: none; }
/* Inline "upload a new logo" affordance inside the Free Vend override modal.
   Needs its own rule because .secondary is only ever styled under
   .modal .actions, .panel-header, .settings-form and .rp-toolbar -- a
   .secondary button anywhere else falls through to unstyled native browser
   chrome, which is exactly the inconsistency rounds 45 #5/#6 had to fix
   twice (Export CSV, and the calendar's Prev/Today/Next). Reuses the same
   --text-on---card-alt pairing those rules use, so it needs no new entry in
   check-color-contrast.js. */
.screen-image-slot { background: var(--card-alt); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; margin: 8px 0; }
.screen-image-slot .muted { display: block; margin: 4px 0 6px; }
/* Same reason as .inline-upload below: .secondary is only styled under
   .modal .actions / .panel-header / .settings-form / .rp-toolbar, so a
   .secondary button in a modal BODY renders as raw browser chrome
   without its own rule (rounds 45 #5/#6). Reuses the already-audited
   --text-on---card-alt pairing, so no new contrast entry is needed. */
.screen-image-slot button.secondary { background: var(--card); color: var(--text); border: 1px solid var(--border); padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; margin-right: 6px; }
.inline-upload { margin: -4px 0 10px; }
/* Same reason as .screen-image-slot / .inline-upload: .secondary is only
   styled under .modal .actions, .panel-header, .settings-form and
   .rp-toolbar. The Remote Pro profile/push controls sit in the modal BODY,
   so without this rule "View settings", "Change settings", "Roll back" and
   the "Download XML" link all render as raw browser chrome -- which is
   exactly how they shipped. Covers a.secondary too, because the download is
   a real link, not a button. Reuses the --text-on---card-alt pairing the
   other two rules use, so check-color-contrast.js needs no new entry. */
.rp-profile-controls button.secondary, .rp-profile-controls a.secondary { background: var(--card-alt); color: var(--text); border: 1px solid var(--border); padding: 4px 10px; border-radius: 6px; cursor: pointer; font-size: 12px; margin-left: 4px; display: inline-block; text-decoration: none; }
.inline-upload button.secondary { background: var(--card-alt); color: var(--text); border: 1px solid var(--border); padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; }
.inline-upload #m-override-upload-row { margin-top: 8px; }
.modal .actions button.danger { background: var(--red); color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; }
.key-box { font-family: monospace; background: var(--card-alt); border: 1px solid var(--border); color: var(--text); padding: 10px; border-radius: 6px; word-break: break-all; font-size: 12px; margin-top: 10px; }
/* Twelfth audit feature (Sessions/Recent-activity rows in the booth-detail
   modal): previously ad hoc inline style="..." strings in app.js, including
   a hardcoded #eee divider (finding #4) invisible to any CSS-only lint --
   moved into real classes so a themed color here can never regress via
   inline style again without check-color-contrast.js's own app.js scanner
   catching it structurally impossible to miss either way. */
.session-row { padding: 6px 0; border-top: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.activity-row { padding: 6px 0; border-top: 1px solid var(--border); }

/* Same form styling as .modal above, for inline (non-modal) settings forms like the Settings tab's Email alerts section. */
.settings-form label { display: block; margin-top: 12px; font-size: 13px; color: var(--muted); }
.settings-form input, .settings-form select { width: 100%; padding: 8px; margin-top: 4px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; background: var(--card-alt); color: var(--text); }
.settings-form input[type="checkbox"] { width: auto; padding: 0; margin: 0 6px 0 0; border: none; vertical-align: middle; }
.settings-form .actions { margin-top: 20px; display: flex; justify-content: flex-start; gap: 8px; }
/* Alert delivery status block (Settings tab / Booths -> Config sub-tab).
   Colours come from the existing themed variables rather than literals, so
   check-color-contrast.js's bare-hex scan stays satisfied and both themes
   track automatically. The status dots are GRAPHICAL indicators, never the
   sole carrier of meaning -- every row also states its condition in words
   ("delivered", "FAILED", "not configured"), so the block stays readable
   without colour perception, which a dot-only design would not. */
.alert-status { background: var(--card-alt); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; font-size: 13px; color: var(--text); }
.alert-status-row { display: flex; align-items: baseline; gap: 8px; padding: 3px 0; }
.alert-status-dot { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
.alert-status-dot.ok { background: var(--green); }
.alert-status-dot.bad { background: var(--red); }
.alert-status-dot.neutral { background: var(--muted); }
/* A <textarea> had NO rule anywhere in this file until the alert-email
   templates were added -- so one fell straight through to native browser
   chrome: white background, black monospace text, browser-default size,
   inline-block so it sat BESIDE its own label instead of under it. On a
   dark-themed form that is unmissable.

   This is the same shape as the .secondary-button gap documented at length
   further up (rounds 34 #9, 45 #5/#6): an element type used in a context
   whose styling only ever enumerated the element types that happened to
   exist at the time. Fixed for BOTH .settings-form and .modal in one go,
   rather than only where it was noticed, because it was already live in the
   other one: composeBoothPush's multi-line settings editor (app.js, the
   2026-09-17 newline fix) renders a <textarea> inside a .modal and has been
   showing as a raw white box since it shipped -- exactly the unverified
   "browser half" that round's own writeup flagged and could not check.

   font-family: inherit is load-bearing, not cosmetic: a textarea defaults
   to monospace while every sibling input inherits the page font, so without
   it the two disagree visibly even once the colours are right.
   resize: vertical keeps a drag-to-resize handle (genuinely useful for a
   long template) without letting it be dragged wider than its container. */
.settings-form textarea, .modal textarea { width: 100%; padding: 8px; margin-top: 4px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; font-family: inherit; line-height: 1.5; background: var(--card-alt); color: var(--text); resize: vertical; display: block; }

/* ---- Booths dashboard (main screen) ---- */
.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 14px; margin-bottom: 24px; }

.booths-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
.booths-grid .brand-header { grid-column: 1 / -1; color: var(--pink-text); font-weight: bold; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; margin: 4px 0; }
.booth-card { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 16px; display: flex; flex-direction: column; gap: 8px; }
.booth-card .booth-card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.booth-card .booth-card-top-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.booth-card .booth-refresh-btn { background: transparent; border: none; color: var(--muted); font-size: 16px; line-height: 1; cursor: pointer; padding: 0; }
.booth-card .booth-refresh-btn:hover { color: var(--text); }
.booth-card .status-line { display: flex; align-items: center; gap: 7px; font-size: 13px; font-weight: bold; }
.booth-card .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.booth-card .status-dot.online { background: var(--green); box-shadow: 0 0 6px var(--green); }
.booth-card .status-dot.offline { background: var(--red); box-shadow: 0 0 6px var(--red); }
.booth-card .status-line.online { color: var(--green-text); }
.booth-card .status-line.offline { color: var(--red-text); }
.booth-card .booth-name { font-weight: bold; font-size: 15px; }
.booth-card .booth-venue { color: var(--muted); font-size: 12px; }
.booth-card .booth-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
.booth-card .booth-lastseen { color: var(--muted); font-size: 11px; }
.booth-card .booth-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
/* REDESIGN PHASE 2b: narrowed to a direct child (was a bare descendant
   selector). The booth-actions row now nests a .segmented control one level
   deep, and at the old (0,2,1) specificity this rule beat every .segmented
   rule (all <= (0,2,1)) and silently flattened the mode toggle back to plain
   card-alt chips. A direct-child combinator still reaches the two buttons
   that genuinely are direct children (Test, the "..." trigger) and stops
   reaching into .segmented's own nested buttons at all. */
.booth-card .booth-actions > button { background: var(--card-alt); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 5px 9px; cursor: pointer; font-size: 12px; }
/* Thirty-fourth audit round: same --red-text-vs-real---card-alt-ancestor fix
   as .data-table td button.danger above -- see that rule's own comment.
   Unused as of Phase 2b (Reboot/Shutdown/Delete moved into the "..."
   popover's own .danger-item styling) -- left in place rather than removed,
   since it costs nothing idle and nothing else in this rework depended on
   deleting it. */
.booth-card .booth-actions > button.danger { color: var(--red-badge-text); border-color: var(--red); }
/* The "..." menu trigger: same transparent-chrome pattern as the existing
   .booth-refresh-btn two rules up, just a touch larger for a comfortable
   tap target and right-aligned by being last in the flex row. */
.booth-card .menu-trigger { background: transparent; border: none; color: var(--muted); font-size: 18px; line-height: 1; cursor: pointer; padding: 4px 8px; border-radius: 6px; margin-left: auto; }
.booth-card .menu-trigger:hover { color: var(--text); background: var(--card-alt); }
/* Selected-state for the override modal's duration-preset row (Phase 2b) --
   reuses --pink, the same accent already used for .drawer-tabs' own active
   underline, rather than inventing a new token for one more "this one is
   picked" indicator. */
.actions button.secondary.active { background: var(--pink); color: #fff; border-color: var(--pink); }
.settings-form button.secondary { background: var(--card-alt); color: var(--text); border: 1px solid var(--border); padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; }

/* ---------------- responsive / mobile ---------------- */
/* The stat-tile, booth-card and dash-toolbar grids already reflow via
   auto-fit/auto-wrap; this handles the parts that don't below ~820px: the
   fixed-height 8-button topbar, wide data tables (esp. the 8-column
   Schedule table), and the multi-column report/form grids. */
@media (max-width: 820px) {
  .topbar { height: auto; flex-wrap: wrap; padding: 8px 12px; gap: 8px 12px; }
  .tabs { flex-wrap: wrap; gap: 2px; order: 3; width: 100%; }
  .who { flex-wrap: wrap; gap: 6px; }
  .tab-btn { padding: 8px 10px; font-size: 13px; }
  main { margin: 16px auto; padding: 0 12px; }

  /* Wide tables scroll horizontally instead of overflowing the viewport.
     display:block lets the table itself become the scroll container while
     its rows keep table layout. */
  .data-table { display: block; overflow-x: auto; white-space: nowrap; }

  /* Side-by-side panels/columns stack. */
  .rp-grid-2 { grid-template-columns: 1fr; }
  .rp-bar-row { grid-template-columns: 110px 1fr 68px; gap: 8px; }
  .panel-header { flex-wrap: wrap; gap: 8px; }
}

/* Round-45 audit (D1): the "alerting is off" banner. Deliberately reuses the
   already-curated --red-text on --card pairing (see check-color-contrast.js's
   ['--red-text','--card','.error text on --card'] entry, which passes in both
   themes) rather than introducing a new colour pair -- this project has hit
   four separate wrong-ancestor contrast false-passes, and every new pair is
   another chance at a fifth. --card is this element's own background here, so
   the curated pair's claimed ancestor is genuinely correct. */
.alerting-banner {
  background: var(--card);
  color: var(--red-text);
  border: 1px solid var(--red-text);
  border-radius: 8px;
  padding: 10px 14px;
  margin-bottom: 14px;
  font-size: 13px;
  line-height: 1.5;
}
/* Round-46 frontend audit: this child rule overrides the background to
   --card-alt while INHERITING --red-text from .alerting-banner, and there was
   no ['--red-text','--card-alt'] pair in check-color-contrast.js -- so the
   combination was invisible to the checker and genuinely failed AA:
   independently recomputed at 4.393:1 dark / 4.282:1 light (both under 4.5).
   Those figures match round 43's F1 to three decimals, which is the SAME pair
   that round already fixed once elsewhere.
   Sixth instance of the wrong-ancestor class (rounds 17 #2, 34 #8, 35 #4,
   44 F1, 43 F1) -- and the parent rule's own comment asserting "--card is this
   element's own background here" was true for the banner and false for this
   child, which the same commit added.
   Fixed with the already-in-tree --red-badge-text (5.438:1 dark / 5.389:1
   light on --card-alt) rather than a new variable, and the pair is now tracked
   in PAIRS so this cannot silently recur. What made it matter: this styles the
   literal `docker-watchdog.ps1 -WebhookUrl`, i.e. the one actionable
   instruction in the banner. */
.alerting-banner code {
  background: var(--card-alt);
  color: var(--red-badge-text);
  padding: 1px 4px;
  border-radius: 3px;
}

/* ---------------------------------------------------------------------
   Adaptive staff view (2026-09-28; selectors updated for Redesign Phase
   2a's #tab-overview markup -- see REDESIGN_PLAN.md).

   Most venues have ONE booth. The fleet layout -- six stat tiles, a search
   box, a venue dropdown, brand headers -- is overhead for a venue staff
   login, telling them things the single card already says. These classes
   are set by applyBoothViewMode() from the number of booths the user can
   actually SEE, so the layout follows the data rather than a setting
   anyone has to choose. Admins are never simplified: the header being
   hidden here also holds "Add booth" / "Venues".
   --------------------------------------------------------------------- */

/* 2-3 booths: cards are still the right shape, the fleet chrome is not. */
#tab-overview.view-small .ov-stat-tiles,
#tab-overview.view-small #booth-search,
#tab-overview.view-small #venue-filter,
#tab-overview.view-single .ov-stat-tiles,
#tab-overview.view-single #booth-search,
#tab-overview.view-single #venue-filter { display: none; }

/* One booth: also drop the brand header grouping a single item. */
#tab-overview.view-single .brand-header { display: none; }

/* Give the single card room to breathe rather than leaving it stranded at
   grid-column width on a wide screen. */
#tab-overview.view-single .booths-grid { grid-template-columns: 1fr; max-width: 560px; }
#tab-overview.view-single .booth-card { padding: 20px; }
#tab-overview.view-single .booth-name { font-size: 20px; }
#tab-overview.view-single .booth-actions button { padding: 8px 12px; font-size: 13px; }

/* The per-booth alerting window is a PAIR of times ("10:00 to 12:00"), so the
   two inputs sit on one line with their joining word between them rather than
   stacking as two unrelated fields. Inputs inside this row must not take the
   full-width `.modal label input` treatment, hence the explicit flex basis. */
.alert-window-row { display: flex; gap: 8px; align-items: center; margin-top: 4px; }
.alert-window-row input { flex: 0 0 90px; margin-top: 0; }

/* The per-venue alert-type checkboxes. Layout only, deliberately: it
   introduces no colour of its own, so it adds nothing for
   check-color-contrast.js to track and cannot drift against a theme. */
.alert-type-picker {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 2px 14px;
  margin-top: 4px;
}
.alert-type-picker label {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 0;
  font-weight: normal;
}
.alert-type-picker input {
  margin-top: 0;
  width: auto;
}

/* =========================================================================
   REDESIGN PHASE 0 -- tokens and shared primitives.
   See REDESIGN_PLAN.md. Additive only: nothing above this line changes,
   nothing below is referenced by any existing screen yet. No visible change
   to the live app until a screen is actually ported in a later phase.
   ========================================================================= */

/* Self-hosted Geist/Geist Mono (public/fonts/*.woff2), not Google Fonts.
   The live CSP (style-src 'self' 'unsafe-inline'; default-src 'self') has
   no font-src/Google allowance at all -- loading from fonts.googleapis.com
   as the design handoff describes would be silently blocked by this
   dashboard's own security header, not a maybe, confirmed by reading the
   real header in src/server.js before choosing this. Both are variable
   fonts: Google serves one physical file per family regardless of which
   weight is requested (confirmed by fetching the real @font-face CSS --
   all four Geist weight declarations and all three Geist Mono ones point
   at the identical URL), so one local file per family covers every weight
   the redesign specifies, same as it does for Google's own CDN delivery. */
@font-face {
  font-family: 'Geist';
  src: url('fonts/Geist-Variable.woff2') format('woff2-variations'), url('fonts/Geist-Variable.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Geist Mono';
  src: url('fonts/GeistMono-Variable.woff2') format('woff2-variations'), url('fonts/GeistMono-Variable.woff2') format('woff2');
  font-weight: 400 600;
  font-style: normal;
  font-display: swap;
}
/* Phase 10 cleanup: .font-ui itself (a speculative "apply Geist to body
   once a screen adopts it") was never actually referenced by any of the
   nine real phases -- body is still plain Arial/Helvetica today -- so it's
   deleted as genuinely unused rather than kept on the old "not yet ported"
   assumption, which no longer applies now that every screen has been.
   Adopting Geist app-wide would be a real, unreviewed visual change and is
   deliberately NOT done here; worth a human decision separately.
   .font-mono IS real and stays -- used by the Schedule slot time and
   activity-log date columns. */
.font-mono { font-family: 'Geist Mono', monospace; }

/* ---- shared components, adopted by the real redesign (header action
   buttons across Overview/Venues/Schedule/Reports/Logos/Users). Does not
   replace the existing bare `button`/`.secondary`/`.danger` rules -- those
   still apply inside .modal/.drawer/.panel-header/etc, scoped per
   container rather than as a reusable class. ---- */

/* Buttons: primary / secondary. Phase 10 cleanup: the originally-planned
   third variant, .btn-danger-outline (plus its .btn-row/.btn-footer size
   modifiers), was never actually adopted by any phase -- removed as
   genuinely dead rather than kept on the old "not yet ported" assumption. */
.btn-primary, .btn-secondary {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: 'Geist', system-ui, sans-serif; font-size: 13px; font-weight: 600;
  padding: 9px 14px; border-radius: 8px; cursor: pointer; border: 1px solid transparent;
  transition: border-color .12s, background-color .12s;
}
.btn-primary { background: var(--pink); color: #fff; border-color: var(--pink); }
.btn-primary:hover { background: var(--pink-hover); border-color: var(--pink-hover); }
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-secondary { background: var(--card-alt); color: var(--text); border-color: var(--border); font-weight: 500; }
.btn-secondary:hover { border-color: var(--border-hover); }
.btn-secondary:disabled { opacity: 0.4; cursor: not-allowed; }

/* Badges: tint background + solid text colour. .red/.amber/.green reuse
   this file's existing, already-allowlisted tint values exactly (same
   rgba() literal as .badge.offline/.mode-pill.FreeVend/.badge.online), so
   they need no new check-color-contrast.js entry. .blue and .pink do NOT
   reuse an existing tint -- rgba(110,168,250,...) is --paid-blue at low
   alpha (110,168,250 = 0x6E,0xA8,0xFA), rgba(236,31,106,...) is --pink
   (236,31,106 = 0xEC,0x1F,0x6A) -- both genuinely new combinations, added
   to ALLOWLISTED_FUNCTIONAL_COLORS in check-color-contrast.js with their
   real composited contrast measured, not assumed. .pink's alpha is 0.10,
   not the handoff's own stated 0.14 -- measured first: 0.14 composited to
   4.484:1 in light theme, just under the 4.5:1 AA floor (--pink-text is
   --pink itself in light theme, with no separate lighter value the way
   dark theme has one -- see --pink-text's own comment above). 0.10 clears
   4.774:1 light / 5.292:1 dark with real margin in both. */
.badge-new { display: inline-block; font-size: 12px; font-weight: 600; padding: 3px 8px; border-radius: 6px; }
.badge-new.small { font-size: 11px; font-weight: 500; }
.badge-new.red { background: rgba(239,68,68,0.15); color: var(--red-badge-text); }
.badge-new.amber { background: rgba(245,166,35,0.15); color: var(--orange); }
.badge-new.green { background: rgba(46,204,113,0.15); color: var(--green-text); }
.badge-new.blue { background: rgba(110,168,250,0.15); color: var(--paid-blue); }
.badge-new.pink { background: rgba(236,31,106,0.10); color: var(--pink-text); }
.badge-new.neutral { background: var(--card-alt); color: var(--text-2); }

/* Segmented control. Bare state: active = bg --border; mode-colouring
   variant (.mode) switches to the orange/blue tint pair instead, per the
   handoff's own "the mode version colours the active option" note.
   rgba(110,168,250,0.2) and rgba(245,166,35,0.18) are two MORE new
   combinations (different alpha from the badge tints above, composited
   against --bg rather than --card since the segmented control sits
   directly on the page background per the handoff spec) -- also measured
   and allowlisted: 5.916/5.134:1 (Paid, dark/light) and 7.141/5.234:1
   (FreeVend, dark/light), all comfortably clear. */
.segmented { display: inline-flex; background: var(--bg); border: 1px solid var(--border); border-radius: 9px; padding: 3px; gap: 2px; }
.segmented button { background: transparent; border: none; color: var(--muted); font-size: 12px; font-weight: 500; padding: 6px 10px; border-radius: 6px; cursor: pointer; }
.segmented button[aria-selected="true"], .segmented button.active { background: var(--border); color: var(--text); font-weight: 600; }
.segmented.mode button.active.Paid { background: rgba(110,168,250,0.2); color: var(--paid-blue); }
.segmented.mode button.active.FreeVend { background: rgba(245,166,35,0.18); color: var(--orange); }

/* Toast. role="status" so it's announced without stealing focus, matching
   this file's existing aria-live conventions elsewhere. Fixed/centred per
   the handoff; the show/hide/auto-dismiss timing lives in app.js
   (Phase 0's showToast() helper), not here. */
.toast-new { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
  background: var(--text); color: var(--bg); font-size: 13px; font-weight: 500;
  padding: 11px 16px; border-radius: 10px; z-index: 500;
  opacity: 0; pointer-events: none; transition: opacity .15s; }
.toast-new.show { opacity: 1; }

/* Drawer: fixed right-hand panel, generalizing this file's existing
   .modal-backdrop/.modal pattern rather than duplicating it -- the two
   share one backdrop element and one focus-trap/generation-token
   mechanism in app.js (openDrawer()/closeDrawer() share the same counter
   as openModal()/closeModal(), see REDESIGN_PLAN.md Phase 0). */
.drawer-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 400; }
.drawer-backdrop.hidden { display: none; }
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: 420px; max-width: 100vw;
  background: var(--card); border-left: 1px solid var(--border);
  display: flex; flex-direction: column; z-index: 401; }
.drawer.wide { width: 480px; }
.drawer.booth-settings { width: 600px; }
.drawer-header { padding: 20px 24px; border-bottom: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between; }
.drawer-header h3 { margin: 0; font-size: 18px; font-weight: 600; color: var(--text); }
.drawer-close { background: transparent; border: none; color: var(--muted); font-size: 18px; cursor: pointer; padding: 4px; }
.drawer-close:hover { color: var(--text); }
.drawer-body { flex: 1; overflow-y: auto; padding: 22px 24px; display: flex; flex-direction: column; gap: 16px; }
.drawer-footer { padding: 16px 24px; border-top: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between; }
.drawer-footer .drawer-footer-right { display: flex; gap: 8px; margin-left: auto; }
.drawer-tabs { display: flex; gap: 4px; padding: 0 24px; border-bottom: 1px solid var(--border); }
.drawer-tabs button { background: transparent; border: none; color: var(--muted);
  font-size: 13px; font-weight: 500; padding: 8px 10px 10px; cursor: pointer;
  border-bottom: 2px solid transparent; margin-bottom: -1px; }
.drawer-tabs button[aria-selected="true"] { color: var(--text); border-bottom-color: var(--pink); }

/* Field styling inside a drawer, mirroring .modal/.settings-form's own
   rules exactly (same background/border/radius/padding/font). Built in
   Phase 0 as shared-primitive CSS, before any real <input>/<select>/
   <textarea> sat inside an actual .drawer -- check-form-element-styling.js's
   FORM_CONTAINERS list was deliberately left without '.drawer' at that
   point, since the check only ever enforces a container against element
   types the real markup actually uses. Phase 2b's Add-booth drawer is that
   first real form content; FORM_CONTAINERS now includes '.drawer' too, and
   passed immediately against this already-waiting rule with no CSS change
   needed for the fields themselves. */
.drawer label { display: block; margin-top: 12px; font-size: 13px; color: var(--text-2); }
.drawer input, .drawer select { width: 100%; padding: 10px 12px; margin-top: 4px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; background: var(--bg); color: var(--text); }
.drawer input[type="checkbox"] { width: auto; padding: 0; margin: 0 6px 0 0; border: none; vertical-align: middle; }
.drawer textarea { width: 100%; padding: 10px 12px; margin-top: 4px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; font-family: inherit; line-height: 1.5; background: var(--bg); color: var(--text); resize: vertical; display: block; }
/* REDESIGN PHASE 2b: the button half of the same gap the field-styling
   comment above already called out. .secondary/.primary are deliberately
   scoped per-container everywhere in this file (.modal .actions, .settings-
   form, .rp-toolbar, ...), never as a bare button.secondary/button.primary
   -- the Add-booth drawer is the first screen to put either class inside a
   real .drawer, and without this a "Copy API key"/"Create" button there
   falls straight through to unstyled native chrome, the exact "one visibly
   inconsistent control" shape this codebase has fixed for Export CSV, the
   calendar's Prev/Today/Next row, and the Logos tab's Delete button. */
.drawer button.secondary, .drawer a.secondary { background: var(--card-alt); color: var(--text); border: 1px solid var(--border); padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; display: inline-block; text-decoration: none; }
.drawer button.primary { background: var(--pink); color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; }
.drawer button.danger { background: var(--red); color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; }

/* Popover: anchored menu (the "..." booth-card menu). A transparent
   full-screen catch layer behind it closes it on outside click, per the
   handoff spec -- built in app.js's openPopover(), this is presentation
   only. */
.popover-catch { position: fixed; inset: 0; z-index: 420; background: transparent; }
.popover { position: absolute; min-width: 190px; background: var(--card-alt);
  border: 1px solid var(--border-strong); border-radius: 10px; padding: 5px;
  box-shadow: 0 12px 32px rgba(0,0,0,0.5); z-index: 421; }
.popover button, .popover a { display: block; width: 100%; text-align: left;
  background: transparent; border: none; color: var(--text); font-size: 13px;
  padding: 8px 10px; border-radius: 6px; cursor: pointer; text-decoration: none; }
.popover button:hover, .popover a:hover { background: var(--border); }
.popover button.danger-item, .popover a.danger-item { color: var(--red-text); }
.popover .popover-divider { height: 1px; background: var(--border); margin: 5px 0; }

/* Stat tile, used both as plain display (Reports) and as a clickable
   filter (Overview's aria-pressed buttons) -- same markup/CSS, only the
   element type and aria-pressed differ at the call site. */
.stat-tile-new { background: var(--card); border: 1px solid var(--border); border-radius: 12px;
  padding: 14px 16px; text-align: left; cursor: default; font-family: inherit; }
button.stat-tile-new { cursor: pointer; }
button.stat-tile-new[aria-pressed="true"] { background: var(--card-alt); border-color: #5A5A68; }
.stat-tile-new .stat-tile-label { display: flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 500; color: var(--text-2); margin-bottom: 6px; }
.stat-tile-new .stat-tile-swatch { width: 7px; height: 7px; border-radius: 2px; flex-shrink: 0; }
.stat-tile-new .stat-tile-value { font-family: 'Geist Mono', monospace; font-size: 28px; font-weight: 500;
  letter-spacing: -0.03em; line-height: 1; color: var(--text); }
.stat-tile-new .stat-tile-value.small { font-size: 24px; }

/* REDESIGN PHASE 2d: bulk-select. #select-booths-btn reuses .btn-secondary's
   own base style (same --card-alt/--border as every other secondary button
   in the header row); only the pink border on activation is new, matching
   the stat-tile [aria-pressed] convention immediately above. */
#select-booths-btn[aria-pressed="true"] { border-color: var(--pink); }

/* The per-card selection checkbox -- a plain round-rect button, not a real
   <input type="checkbox">, so its own focus/keyboard activation matches
   every other icon-button in this file (Enter/Space, native). Unchecked:
   transparent with a visible border. Checked: solid pink with a check mark,
   exact colors from the design handoff's own b.checkBg/checkBorder. */
.booth-select-checkbox { width: 20px; height: 20px; border-radius: 6px; border: 1.5px solid var(--border-hover);
  background: transparent; color: #fff; font-size: 12px; line-height: 1; cursor: pointer; padding: 0;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 2px; }
.booth-select-checkbox.checked { border-color: var(--pink); background: var(--pink); }
.booth-select-wrap { display: flex; gap: 10px; align-items: flex-start; }
.booth-card.selected { border-color: var(--pink); }

/* The floating bulk-action bar, fixed to the bottom center of the viewport
   regardless of scroll -- shown only while selecting with >=1 booth picked
   (toggled in app.js's renderBulkBar(), called from the one place
   renderBoothsGrid() already re-derives every other piece of Overview/
   Booths UI state, so this can never drift from the real selection array). */
#bulk-bar { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); z-index: 350;
  background: var(--card-alt); border: 1px solid var(--border-strong); border-radius: 12px;
  padding: 8px 8px 8px 16px; display: flex; align-items: center; gap: 12px;
  box-shadow: 0 12px 32px rgba(0,0,0,0.5); }
#bulk-bar.hidden { display: none; }
#bulk-bar .bulk-bar-count { font-size: 13px; font-weight: 500; white-space: nowrap; }
#bulk-bar button.bulk-clear { background: transparent; border: none; color: var(--muted); font-size: 13px; cursor: pointer; }
#bulk-bar button.bulk-clear:hover { color: var(--text); }
#bulk-bar button.bulk-change { background: var(--pink); color: #fff; border: none; border-radius: 8px;
  padding: 8px 14px; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; }

/* The bulk-change drawer's own field rows (edit step) and review rows
   (review step). Reuses .drawer label/select styling as-is for the edit
   step's selects; these two rules are only for the parts that have no
   existing analog -- the "changed" pink border on a field that is no
   longer "Leave unchanged", and the review step's boothId-pill list. */
.bulk-field-select.changed { border-color: var(--pink) !important; }
.bulk-review-rows { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
.bulk-review-row { display: flex; justify-content: space-between; gap: 12px; padding: 12px 14px; font-size: 13px; flex-wrap: wrap; }
.bulk-review-row + .bulk-review-row { border-top: 1px solid var(--border); }
.bulk-review-row .bulk-review-value { color: var(--orange); font-family: 'Geist Mono', monospace; font-size: 12px; }
.bulk-review-pills { display: flex; flex-wrap: wrap; gap: 6px; }
.bulk-review-pill { font-size: 12px; background: var(--card-alt); border: 1px solid var(--border); border-radius: 6px; padding: 3px 8px; }

/* Status dot, the one atom used everywhere (booth cards, Needs-attention
   rows, Users table, login activity). */
.status-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.status-dot.online { background: var(--green); }
.status-dot.offline { background: var(--red); }
.status-dot.amber { background: var(--orange); }

/* =========================================================================
   REDESIGN PHASE 1 -- Guide tab pilot. See REDESIGN_PLAN.md. Scoped under a
   .guide- prefix throughout (checked against the pre-change markup for a
   collision before writing a single rule -- none found), so this cannot
   touch anything outside #tab-guide. The FIRST real use of the self-hosted
   Geist/Geist Mono fonts added in Phase 0 -- everything else in the app
   still renders in Arial until a later phase ports it.
   ========================================================================= */

#tab-guide { font-family: 'Geist', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
.guide-page { max-width: 1200px; margin: 0 auto; display: flex; gap: 56px; align-items: flex-start; flex-wrap: wrap; }
.guide-nav { flex: 0 0 220px; position: sticky; top: 20px; display: flex; flex-direction: column; gap: 2px; font-size: 13px; line-height: 1.4; }
.guide-nav-label { font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); padding: 0 12px 10px; }
.guide-nav a { color: var(--text-2); padding: 7px 12px; border-radius: 8px; text-decoration: none; }
.guide-nav a:hover { background: var(--card); color: var(--text); }
.guide-article { flex: 1 1 560px; min-width: 0; max-width: 760px; display: flex; flex-direction: column; gap: 56px; }
.guide-article h1 { margin: 0; font-size: 34px; font-weight: 600; letter-spacing: -0.025em; line-height: 1.15; }
.guide-article > p.guide-lede { margin: 10px 0 0; color: var(--text-2); font-size: 16px; text-wrap: pretty; }
.guide-section { display: flex; flex-direction: column; gap: 16px; }
.guide-section h2 { margin: 0; font-size: 22px; font-weight: 600; letter-spacing: -0.015em; display: flex; gap: 12px; align-items: baseline; }
.guide-num { font-family: 'Geist Mono', monospace; font-size: 14px; color: var(--pink-text); }
.guide-section p { margin: 0; text-wrap: pretty; line-height: 1.6; }
.guide-section .muted-2 { color: var(--text-2); }

/* The "which mode wins" chain and the step/proves/doesn't-prove cards all
   share this one card shell. */
.guide-card { background: var(--card); border: 1px solid var(--border); border-radius: 14px; padding: 20px 22px; display: flex; flex-direction: column; gap: 14px; }
.guide-card-label { font-size: 13px; font-weight: 600; }
.guide-chain { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 13px; }
.guide-chain-step { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 6px 10px; }
.guide-chain-arrow { color: var(--faint); }
.guide-chain-end { background: rgba(110,168,250,0.12); color: var(--paid-blue); border-radius: 8px; padding: 6px 10px; font-weight: 600; }

/* Label/value "table" rendered as stacked grid rows, replacing the old
   .data-table usage for this one screen -- matches the handoff's own
   layout exactly (fixed label column, border between rows, no header). */
.guide-rows { background: var(--card); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; font-size: 14px; }
.guide-row { display: grid; grid-template-columns: 150px minmax(0,1fr); gap: 16px; padding: 13px 20px; }
.guide-row + .guide-row { border-top: 1px solid var(--border); }
.guide-rows.wide .guide-row { grid-template-columns: 220px minmax(0,1fr); gap: 20px; padding: 15px 20px; font-size: 13px; }
.guide-rows.deploy .guide-row { grid-template-columns: 180px minmax(0,1fr); gap: 16px; padding: 12px 18px; font-size: 13px; }
.guide-row-label { color: var(--muted); }
.guide-row-title { font-weight: 600; }

.guide-code { font-family: 'Geist Mono', monospace; font-size: 12.5px; background: var(--bg); border: 1px solid var(--border); border-radius: 5px; padding: 1px 6px; }
.guide-code-inline { font-family: 'Geist Mono', monospace; font-size: 13px; }
.guide-code-break { font-family: 'Geist Mono', monospace; font-size: 12px; word-break: break-all; }

/* Step-3-cards and the Proves/Doesn't-prove pair share this grid+card
   pattern. */
.guide-grid-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
.guide-grid-cards.wide { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.guide-step-card { background: var(--card); border: 1px solid var(--border); border-radius: 14px; padding: 18px; display: flex; flex-direction: column; gap: 8px; }
.guide-step-label { font-family: 'Geist Mono', monospace; font-size: 12px; color: var(--muted); }
.guide-step-title { font-weight: 600; }
.guide-step-body { font-size: 13px; color: var(--text-2); line-height: 1.55; }
.guide-step-card.proves .guide-step-title { color: var(--green-text); font-size: 13px; }
.guide-step-card.doesnt-prove .guide-step-title { color: var(--red-text); font-size: 13px; }

/* File chips (Paid.xml/FreeVend.xml, the four screen images). */
.guide-chips { display: flex; gap: 10px; flex-wrap: wrap; }
.guide-chip { flex: 1 1 200px; background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; display: flex; flex-direction: column; gap: 4px; }
.guide-chip code { font-family: 'Geist Mono', monospace; font-size: 13px; }
.guide-chip span { font-size: 13px; color: var(--muted); }

/* Callouts -- red for a hard blocker, amber for a warning. Two MORE
   genuinely new tints (0.08 alpha, not reused from anywhere else in this
   file) -- measured and allowlisted in check-color-contrast.js, same as
   the badge/segmented tints added in Phase 0: body text (--text) and the
   <strong> emphasis (--red-text / --orange) both clear 4.5:1 against the
   real composited background in both themes, tightest case 5.059:1. */
.guide-callout { border-radius: 12px; padding: 14px 16px; font-size: 14px; line-height: 1.55; text-wrap: pretty; }
.guide-callout.red { background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.3); }
.guide-callout.red strong { color: var(--red-text); }
.guide-callout.amber { background: rgba(245,166,35,0.08); border: 1px solid rgba(245,166,35,0.3); }
.guide-callout.amber strong { color: var(--orange); }

h3.guide-sub { margin: 0; font-size: 16px; font-weight: 600; }
.guide-sub-block { display: flex; flex-direction: column; gap: 10px; }

@media (max-width: 820px) {
  .guide-nav { position: static; flex: 1 1 100%; flex-direction: row; flex-wrap: wrap; }
}

/* =========================================================================
   REDESIGN PHASE 2a -- Topbar + Overview (Booths). See REDESIGN_PLAN.md.

   The old .topbar/.brand/.tabs/.who rules above are left exactly as they
   were, now simply unreferenced -- index.html's header uses .topbar-new/
   .topbar-brand/.topbar-nav/.topbar-who instead. Same "coexist during
   migration" reasoning as Phase 0's own primitives comment: nothing else
   in this file depends on them being removed, and removing them would be
   a pointless risk for zero benefit.

   .tab-btn ITSELF is still the live class on every nav button -- app.js's
   generic `.tab-btn[data-tab]` click wiring depends on that exact class
   name, so its own base rule (color:#cfd8e3, built for the navy .topbar)
   still applies by default. That value is illegible against this new
   card-colored topbar in EITHER theme (near-invisible on a white light-
   theme card), so it needs a themed override here, not a rename -- scoped
   under .topbar-new so it only changes inside the new topbar, never the
   shared rule a differently-scoped future use of .tab-btn might still
   want.
   ========================================================================= */

.topbar-new {
  display: flex; align-items: center; gap: 24px;
  background: var(--card); border-bottom: 1px solid var(--border);
  padding: 0 20px; height: 56px;
  font-family: 'Geist', system-ui, sans-serif; -webkit-font-smoothing: antialiased;
}
.topbar-brand { display: flex; align-items: center; gap: 10px; cursor: pointer; flex-shrink: 0; }
.topbar-brand-tile {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 8px;
  background: var(--pink); color: #fff; font-weight: 700; font-size: 14px;
}
.topbar-brand-name { font-weight: 600; font-size: 15px; color: var(--text); }
.topbar-nav { display: flex; gap: 4px; flex: 1 1 auto; overflow-x: auto; }
/* Beats the shared .tab-btn rule (0,1,0) on specificity (0,2,0) regardless
   of source order, same pattern this file already uses for .panel-header
   button.secondary beating the bare .panel-header button rule. */
.topbar-new .tab-btn { color: var(--text-2); }
.topbar-new .tab-btn.active, .topbar-new .tab-btn:hover { background: var(--card-alt); color: var(--text); }
.topbar-who { display: flex; align-items: center; gap: 10px; font-size: 13px; flex-shrink: 0; }
.topbar-who-name { color: var(--text-2); margin-right: 2px; white-space: nowrap; }
.topbar-theme-btn {
  background: transparent; border: 1px solid var(--border); color: var(--text-2);
  width: 30px; height: 30px; border-radius: 8px; cursor: pointer;
}
.topbar-theme-btn:hover { border-color: var(--border-hover); color: var(--text); }

/* ---- Overview (#tab-overview) ---- */

.ov-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin-bottom: 20px; }
.ov-header h1 { margin: 0 0 4px; font-size: 26px; font-weight: 600; letter-spacing: -0.02em; color: var(--text); }
.ov-header-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
#booth-search {
  background: var(--card-alt); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 12px; font-size: 13px; color: var(--text); min-width: 180px;
}
#booth-search::placeholder { color: var(--muted); }
.ov-header-right #venue-filter {
  background: var(--card-alt); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 12px; font-size: 13px; color: var(--text);
}

.ov-stat-tiles { display: grid; grid-template-columns: repeat(6, minmax(0,1fr)); gap: 10px; margin-bottom: 20px; }

.ov-body { display: grid; grid-template-columns: minmax(0,1fr) 300px; gap: 20px; align-items: start; }
.ov-main { min-width: 0; }
.ov-rail { min-width: 0; }

.ov-card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; margin-bottom: 16px; }
.ov-rail .ov-card { margin-bottom: 0; }
.ov-card .panel-header { margin-bottom: 10px; }
.ov-card .panel-header h3 { margin: 0; font-size: 14px; font-weight: 600; color: var(--text); }

.ov-rail-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

.ov-empty-state { text-align: center; padding: 48px 20px; color: var(--muted); font-size: 14px; display: flex; flex-direction: column; align-items: center; gap: 14px; }

/* REDESIGN PHASE 3: Venues page. #tab-venues has no role="tabpanel"/
   aria-labelledby the way every other panel does -- it is reached by a
   link, not a role="tab" trigger, so there is no tab element for it to be
   labelled by; its own visible <h1> is its accessible name instead. */
.venues-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin: 14px 0 20px; }
.venues-header h1 { margin: 0 0 6px; font-size: 26px; font-weight: 600; letter-spacing: -0.02em; color: var(--text); }
.venues-header .subtitle { max-width: 620px; }
.venues-card { background: var(--card); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; }
/* .brand-header already exists scoped to .data-table/.booths-grid (see
   those rules above) -- this is a third, equally scoped instance of the
   same class for the same meaning (a section heading grouping venues by
   brand), not a new concept. */
.venues-card .brand-header { padding: 9px 20px; background: var(--card-alt); color: var(--pink-text); font-weight: bold; font-size: 12px; text-transform: uppercase; letter-spacing: 0.03em; border-top: 1px solid var(--border); }
.venues-card .brand-header:first-child { border-top: none; }
.venue-row { display: flex; align-items: center; gap: 20px; padding: 16px 20px; flex-wrap: wrap; border-top: 1px solid var(--border); }
.venue-row-main { flex: 1 1 200px; display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.venue-row-main strong { font-size: 15px; color: var(--text); }
.venue-row-status { flex: 1 1 200px; display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-2); }
.venue-row-status.missing { color: var(--orange); }
.venue-row-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.venues-empty-row { padding: 20px; color: var(--muted); font-size: 13px; }
/* Plain, un-classed <button>s inside .venues-card -- same "a container's
   own button styling has to be given explicitly or it falls through to
   unstyled native browser chrome" lesson this file documents repeatedly
   elsewhere (.data-table td button and its many siblings). Mirrors
   .data-table td button's exact shape rather than inventing a new one. */
.venue-row-actions button { background: var(--card-alt); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 5px 9px; cursor: pointer; font-size: 12px; }
.venue-row-actions button.danger { color: var(--red-badge-text); border-color: var(--red); }
/* button:disabled (global, !important, round 44) already covers the
   disabled Delete button -- no component-scoped rule needed here. */

.pink-link { color: var(--pink-text); font-size: 12px; font-weight: 600; text-decoration: none; }
.pink-link:hover { text-decoration: underline; }

/* Needs-attention panel. Red-tinted border on the panel itself (same
   rgba(239,68,68,...) family already allowlisted for .guide-callout.red/
   .badge-new.red above) and on a flagged booth's own card, so the two
   stay visually linked without needing a second contrast review -- the
   border never carries text, so it is decorative only and out of
   check-color-contrast.js's scope exactly like every other border color
   in this file. */
#needs-attention-panel { border-color: rgba(239,68,68,0.35); }
.booth-card.has-issue { border-color: rgba(239,68,68,0.45); }

.na-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 0; }
.na-row + .na-row { border-top: 1px solid var(--border); }
.na-row-main { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; min-width: 0; }
.na-row-reasons { color: var(--muted); }
.btn-secondary.small { padding: 6px 10px; font-size: 12px; flex-shrink: 0; }

@media (max-width: 980px) {
  .ov-body { grid-template-columns: 1fr; }
  .ov-stat-tiles { grid-template-columns: repeat(3, minmax(0,1fr)); }
}
@media (max-width: 820px) {
  .topbar-new { flex-wrap: wrap; height: auto; padding: 10px 16px; }
  .topbar-nav { order: 3; width: 100%; }
  .ov-stat-tiles { grid-template-columns: repeat(2, minmax(0,1fr)); }
}

/* ---- Redesign Phase 9: Sign-in screen + topbar avatar-link + Account ---- */

/* Sign-in screen: brand mark + card + footer, stacked in a centred column
   (was previously centering a single child; now three persistent siblings,
   only one of the three form cards ever hidden/shown among them). */
.login-screen { flex-direction: column; gap: 24px; }
.signin-brand { display: flex; align-items: center; gap: 10px; }
.signin-brand-tile {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 8px;
  background: var(--pink); color: #fff; font-weight: 700; font-size: 16px;
}
.signin-brand-name { font-weight: 600; font-size: 17px; color: var(--text); }
.login-footer { margin: 0; color: var(--faint); font-size: 12px; text-align: center; max-width: 380px; line-height: 1.5; }

/* The 2FA code input, both on sign-in (24px) and reused smaller (18px, via
   .small) inside the Account page's own setup card. Scoped to BOTH real
   containers it lives in (.login-card, .settings-form) rather than left as
   a bare .totp-code-input: each container's own generic "input" rule
   (.login-card input / .settings-form input) is a class+element selector,
   exactly as specific as a bare .totp-code-input class, so it silently won
   the cascade and flattened this to a plain 14px field -- found live, not
   in review, by the Phase 9 verification harness asserting the real
   computed font-size. input.totp-code-input alone would only have tied
   again; scoping to the container is what actually wins regardless of
   source order. */
.login-card input.totp-code-input,
.settings-form input.totp-code-input { text-align: center; font-family: 'Geist Mono', monospace; font-size: 24px; letter-spacing: 0.4em; }
.login-card input.totp-code-input.small,
.settings-form input.totp-code-input.small { font-size: 18px; letter-spacing: 0.3em; width: 180px; }
/* Same specificity collision as the .totp-code-input fix just above: its
   one real use (#totp-back-btn) sits inside .login-card, whose own
   ".login-card button" rule (class+element) outranks a bare ".text-link-btn"
   class and was silently flattening "Use a different account" into a solid
   pink block identical to Verify -- found by actually looking at the
   captured screenshot, not by any assertion (nothing asserted its look,
   only that it functions). Scoped to the real container for the same
   reason: a tie broken only by source order is fragile to the next edit. */
.login-card button.text-link-btn { background: transparent; border: none; color: var(--muted); font-size: 13px; cursor: pointer; padding: 0; margin-top: 10px; width: auto; }
.login-card button.text-link-btn:hover { color: var(--text); }

/* Activate screen's password-strength meter. Bars default to --border
   (grey, nothing typed yet); updateActivatePasswordStrength() in app.js
   sets each bar's background directly per keystroke, since the colour
   depends on a live score, not a fixed class. */
.pw-strength { margin-top: -8px; display: flex; flex-direction: column; gap: 6px; }
.pw-strength-bars { display: flex; gap: 4px; }
.pw-strength-bar { flex: 1; height: 4px; border-radius: 2px; background: var(--border); }
.pw-strength-text { font-size: 12px; color: var(--muted); }
.pw-strength-text.weak { color: var(--red-text); }

/* Topbar: the old separate Change-password/2FA/Sign-out buttons are gone --
   the whole name+avatar is now one clickable link to Account. */
.topbar-account-link { display: flex; align-items: center; gap: 10px; cursor: pointer; border-radius: 8px; padding: 2px 4px; }
.topbar-account-link:hover .topbar-who-name { color: var(--text); }
.topbar-account-link:focus-visible { outline: 2px solid var(--pink); outline-offset: 2px; }

/* 52px variant of the same .user-avatar chip Users/the topbar already use,
   for the Account page's own header -- a modifier, not a duplicate, so the
   base shape (background/border/radius/centering) can't drift between the
   two sizes. */
.user-avatar.lg { width: 52px; height: 52px; font-size: 18px; }

.account-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin: 14px 0 20px; }
.account-header-who { display: flex; align-items: center; gap: 16px; }
.account-header-stack { display: flex; flex-direction: column; gap: 3px; }
.account-header-stack h1 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: -0.02em; color: var(--text); }
/* Same shape as .panel-header button.secondary beating the bare
   .panel-header button rule -- a dedicated container-scoped rule, not a
   bare button.secondary (see style.css's own repeated notes on why a plain
   .primary/.secondary outside a tracked container renders as unstyled
   native chrome). */
.account-header button.secondary { background: var(--card-alt); color: var(--text); border: 1px solid var(--border); padding: 9px 14px; border-radius: 8px; cursor: pointer; font-size: 13px; }
.account-header button.secondary:hover { border-color: var(--border-hover); }

.account-card-subtitle { display: block; font-size: 12px; margin: 2px 0 14px; }
.account-pw-form { max-width: 420px; }

/* Appearance: three illustrative mini theme previews. The swatch colours
   are DELIBERATELY not var(--bg)/var(--card) -- all three cards must show
   what each theme looks like regardless of which theme is currently
   active, so each needs its own fixed representation of dark/light,  not
   whatever the live theme resolves to right now. Same "deliberately
   unthemed" exception already established for .topbar/.tab-btn's
   --navy/#cfd8e3 -- registered in check-color-contrast.js's
   ALLOWLISTED_HEX_LITERALS for the same reason. No text ever sits on top
   of these, so contrast isn't a concern, only the bare-hex-literal scan. */
.account-theme-grid { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 10px; }
.account-theme-card { background: var(--bg); border: 1.5px solid var(--border); border-radius: 12px; padding: 12px; cursor: pointer; display: flex; flex-direction: column; gap: 10px; color: var(--text); text-align: left; font-size: 13px; font-weight: 500; }
.account-theme-card.active { border-color: var(--pink); }
.account-theme-swatch { height: 56px; border-radius: 8px; border: 1px solid var(--border); display: flex; align-items: flex-end; padding: 8px; gap: 4px; box-sizing: border-box; }
.account-theme-bar { width: 40%; height: 8px; border-radius: 3px; }
.account-theme-chip { width: 18%; height: 8px; border-radius: 3px; background: var(--pink); }
.account-theme-swatch.system { background: linear-gradient(90deg, #0B0B10 50%, #F4F5F9 50%); }
.account-theme-swatch.system .account-theme-bar { background: var(--muted); }
.account-theme-swatch.light { background: #F4F5F9; }
.account-theme-swatch.light .account-theme-bar { background: #D9DCE6; }
.account-theme-swatch.dark { background: #17171E; }
.account-theme-swatch.dark .account-theme-bar { background: #2A2A35; }

/* Two-factor: idle/setup/disable are mutually exclusive, toggled via
   .hidden like every other form-swap in this file (login/totp/activate
   above). */
.account-tfa-idle { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; padding-top: 4px; }
.account-tfa-idle-text { max-width: 420px; line-height: 1.5; font-size: 13px; }
.account-tfa-idle button { padding: 9px 14px; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 600; }
.account-tfa-idle button.primary { background: var(--pink); color: #fff; border: none; }
.account-tfa-idle button.primary:hover { background: var(--pink-hover); }
.account-tfa-idle button.account-tfa-turnoff { background: transparent; color: var(--red-text); border: 1px solid var(--border); }
.account-tfa-idle button.account-tfa-turnoff:hover { border-color: var(--border-hover); }
.account-tfa-setup { display: flex; gap: 24px; flex-wrap: wrap; align-items: flex-start; padding-top: 4px; }
.account-tfa-qr { width: 168px; height: 168px; background: #fff; border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.account-tfa-qr img { display: block; border-radius: 4px; }
.account-tfa-setup-right { flex: 1 1 260px; display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.account-tfa-steps { margin: 0; padding-left: 18px; color: var(--text-2); font-size: 13px; line-height: 1.7; }
.account-tfa-form .actions { margin-top: 14px; }
.account-tfa-disable { display: flex; flex-direction: column; gap: 14px; padding-top: 4px; }
.warn-callout { background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.3); border-radius: 10px; padding: 12px 14px; font-size: 13px; color: var(--text); line-height: 1.5; }
