/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ===========================================================================
   app.css - QRAUZ shared responsive foundation
   ---------------------------------------------------------------------------
   Linked by every page (before its inline <style>) and copied verbatim to
   public/ by build.js. It is intentionally LOW-RISK:

   - The --q-* design tokens are OPT-IN. Linking this file changes nothing
     until a rule actually uses var(--q-*). The 23 pages are individually
     inline-styled with no shared reset, so anything that touched bare element
     selectors globally (e.g. box-sizing) would shift tuned layouts. We avoid
     that. Pages adopt the tokens incrementally (Phase 3).
   - Only two genuinely-safe global rules ship: iOS text-size-adjust and a
     media overflow guard.
   - Utility classes (.q-*) are unused by current markup, so they're inert
     until applied.

   BOUNDED-FLUID approach (the project's chosen method): sizes scale with the
   viewport via clamp(min, base + vw, max) so they grow/shrink with the screen
   but never below a readable/tappable floor or above an oversized ceiling.
   This is the safe version of "percentage-based UI that depends on resolution".

   Colour note: CSS-variable names are inconsistent across pages (index.html
   uses --accent/--hd/--white; other pages use --primary-accent/--header-dark/
   --highlight). Any rule here that needs a colour must use a fallback chain,
   e.g. var(--primary-accent, var(--accent, #7289da)).
   =========================================================================== */

/* ===========================================================================
   THEME (v16.47) - DARK default, LIGHT opt-in; themed CENTRALLY.
   ---------------------------------------------------------------------------
   THE SPECIFICITY TRICK: every page declares its own `:root { --bg:#23272a; }`
   in its inline <style>, which loads AFTER this file and would normally win.
   `html[data-theme="..."]` has specificity (0,1,1) vs `:root`'s (0,1,0), so
   these blocks OVERRIDE the per-page palettes without editing 33 files.
   That means theming is centralised here - do NOT re-add palettes to pages.

   Two naming schemes exist historically (see Colour note above): index.html
   uses --bg/--fg/--card/--accent/--white; the rest use --background/
   --foreground/--primary-accent/--highlight. We set BOTH from one semantic
   source so either scheme themes correctly.

   THE INVERSION THAT MATTERS: --white and --highlight are used as TEXT colours
   (white text on dark). In light mode they must become near-BLACK, or light
   mode renders white-on-white. That is why a semantic layer is required rather
   than just swapping backgrounds.

   data-theme is set on <html> before first paint by the inline bootstrap in
   each page's <head> (no flash). Default = DARK. LIGHT is a SECONDARY theme the
   user opts into via Account > Appearance (owner decision 2026-07-18), so an
   unset/failed lookup must always resolve to dark.
   =========================================================================== */

/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] {
  /* Semantic source of truth (light). A cool neutral grey-blue page with WHITE
     cards: it had to sit with the blurple accent, and the earlier warm/green
     off-white fought it. SIX surface steps, because a light UI builds depth by
     stacking pale tints where dark built it by stacking dark ones:
        bg  <  well  <  surface-2  <  hover  <  raised  <  surface (white)   */
  --q-bg:            #eff1f6;  /* page background                           */
  --q-surface:       #ffffff;  /* cards / panels sit ON the page bg          */
  --q-surface-2:     #eaedf4;  /* inputs + secondary buttons ON a card       */
  --q-well:          #e3e7f0;  /* deepest inset: image wells, skeletons      */
  --q-raised:        #f5f7fb;  /* bubble / embed raised ON a card            */
  --q-hover:         #e2e6f0;  /* hover fill for the above                   */
  --q-header:        #ffffff;  /* header bar                                 */
  --q-text:          #14161c;  /* primary text                              */
  --q-text-muted:    #565e70;  /* secondary text          (7.0:1 on white)   */
  --q-text-dim:      #98a0b2;  /* disabled / past / placeholder              */
  --q-border:        #e2e6ef;  /* hairline dividers                          */
  --q-border-strong: #ccd2e0;  /* outlined buttons, 2px selectable chips     */
  --q-accent:        #525eb8;  /* brand blurple, DEEPENED for light: #7289da
                                  carries only 3.0:1 against white, and it is
                                  used BOTH as a fill under white text and as
                                  link/heading text. Dark keeps #7289da.

                                  v16.95 (2026-08-30): WAS #5b68cc. DO NOT PUT
                                  IT BACK. #5b68cc was chosen against WHITE
                                  only, where it clears 4.6:1 - but accent text
                                  does not only sit on white cards, and on
                                  every OTHER light ground it failed the 4.5:1
                                  normal-text bar:
                                     --q-bg      #eff1f6 (page)   4.32  FAIL
                                     --q-surface-2 #eaedf4        4.17  FAIL
                                     --q-well    #e3e7f0         3.95  FAIL
                                     --q-hover   #e2e6f0         3.91  FAIL
                                     --q-accent-wash #e8ebf9     4.12  FAIL
                                  Found via settings.html's foot link (§73.6),
                                  but it was never a settings bug - it hit any
                                  accent link not sitting on a white card, on
                                  all 43 pages.
                                  #525eb8 is the SMALLEST darkening that clears
                                  4.5:1 on EVERY light ground (page 5.09, white
                                  5.75, wells/hover 4.60-4.64, as a fill under
                                  white text 5.75 - up from 4.89, so buttons
                                  improved too). Same hue, ~10% darker, chosen
                                  minimal precisely so light mode still looks
                                  like itself - Rogan picked this over the
                                  larger #4650b0 for that reason.
                                  A per-selector repair sheet was the
                                  alternative and was rejected: ~93 candidate
                                  rules across 37 files, certain to miss some,
                                  and every new page would reintroduce it.
                                  DARK IS UNAFFECTED - this token only exists
                                  inside html[data-theme="light"].            */
  --q-accent-ink:    #4650b0;  /* accent for small/bold text on light        */
  --q-accent-wash:   #e8ebf9;  /* accent-tinted fill for selected states     */
  --q-success:       #1f9d63;
  --q-danger:        #d33a3a;
  /* v16.92: the danger counterpart to --q-accent-wash - a tinted FILL, not a
     text colour, for a panel that carries a "we cannot host this" message.
     Added because the category dropdown's refusal banner needed one and there
     was no danger tint in the palette, only the solid --q-danger. Same job and
     same weight as --q-accent-wash, so it is defined next to its sibling. */
  --q-danger-wash:   #fbeaea;
  --q-warning:       #b56a00;
  --q-gold:          #e0a000;
  --q-shadow:        0 2px 10px rgba(20,22,28,0.08);
  --q-shadow-pop:    0 8px 28px rgba(20,22,28,0.14);

  /* Legacy scheme A (index.html) */
  --bg: var(--q-bg);       --fg: var(--q-surface);   --hd: var(--q-header);
  --card: var(--q-surface); --border: var(--q-border);
  --accent: var(--q-accent); --muted: var(--q-text-muted);
  --white: var(--q-text);   /* INVERTED: used as text colour */
  --star: var(--q-gold);
  /* Legacy scheme B (all other pages) */
  --background: var(--q-bg);        --foreground: var(--q-surface);
  --header-dark: var(--q-header);   --primary-accent: var(--q-accent);
  --secondary-accent: var(--q-text-muted);
  --highlight: var(--q-text);       /* INVERTED: used as text colour */
  --gold: var(--q-gold);
  /* Shared status colours (both schemes) */
  --success: var(--q-success); --danger: var(--q-danger); --warning: var(--q-warning);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="dark"] {
  /* DELIBERATELY EMPTY. Dark is the shipped look and every page already
     declares its own correct palette in its inline :root. Anything set here
     would OVERRIDE those pages - which is exactly how the dark theme got
     changed by accident: this block used to set --gold:#feca1b, silently
     recolouring the stars on calendars.html and review.html, which both use
     #f1c40f. Dark must render byte-identical to what shipped, so this block
     stays empty and ALL theming work happens under html[data-theme="light"].
     See [[feedback_never_change_looks]]. */
}

/* ===========================================================================
   LIGHT-MODE REPAIR SHEET
   ---------------------------------------------------------------------------
   The 33 pages were written on a dark canvas and hard-code ~275 dark values
   (#202225 inputs, #1a1c1f wells, #2a2d31 bubbles, #333/#444 borders, ...).
   Those cannot be re-pointed by swapping a variable, because they are literal
   hexes - and they must NOT be edited in the pages, because that is exactly
   how the loading skeletons got wrecked before: a page hex was swapped for a
   token whose DARK value differed, so dark changed too.

   So every repair lives HERE, scoped to `html[data-theme="light"]`. That scope
   IS the safety guarantee: a rule that can only match when data-theme="light"
   is structurally incapable of altering dark mode. Never add a light repair to
   a page's inline <style>.

   Specificity: `html[data-theme="light"]` is (0,1,1), so these beat the pages'
   own (0,1,0) / (0,2,0) rules without !important. Two consequences to respect:
     - a repair also outranks the page's `:hover` / `.active` variants, so any
       state a repair covers must be RE-STATED here (see the selected-chip and
       .star.filled blocks) or it is silently flattened;
     - runtime-injected CSS (auth-ui.mjs, dialog.mjs) lands in <head> AFTER
       this file, but specificity still wins, so those are repaired here too.

   Selectors are grouped by ROLE, not by page, so a new page that reuses the
   site-standard vocabulary (.btn-secondary, .listing-image, .form-group input)
   is themed the day it ships.
   =========================================================================== */

/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] body { background: var(--q-bg); color: var(--q-text); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- Site chrome ---------------------------------------------------------
   Header and footer are white in light, so the heavy black drop shadow they
   carry for dark (`0 2px 8px #0005`) reads as a smudge. Swap for a hairline
   plus a whisper of shadow, and give the footer a top rule so it does not
   merge into the page. */
html[data-theme="light"] header {
  box-shadow: 0 1px 0 var(--q-border), 0 2px 12px rgba(20,22,28,0.05);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] footer { border-top: 1px solid var(--q-border); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* The brand pill is tuned for a dark bar; on white the 8% accent fill all but
   vanishes. Nudge both up so the lockup still reads as a deliberate badge. */
html[data-theme="light"] header .logo {
  border-color: rgba(91,104,204,0.30);
  background: rgba(91,104,204,0.07);
}
html[data-theme="light"] header .logo:hover { background: rgba(91,104,204,0.13); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Hero washes. help.html fades from a hard #1f2226 into the page background,
   which in light is a dark bruise across the top of the page (reported). Fade
   from white instead so the hero still lifts off the page. */
html[data-theme="light"] .hero {
  background: linear-gradient(180deg, var(--q-surface) 0%, var(--q-bg) 100%);
  border-bottom: 1px solid var(--q-border);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- ROLE: form controls (pages fill these #202225 with a #333 border) --- */
html[data-theme="light"] input[type="text"],
html[data-theme="light"] input[type="email"],
html[data-theme="light"] input[type="password"],
html[data-theme="light"] input[type="number"],
html[data-theme="light"] input[type="search"],
html[data-theme="light"] input[type="tel"],
html[data-theme="light"] input[type="url"],
html[data-theme="light"] input[type="date"],
html[data-theme="light"] textarea,
html[data-theme="light"] select,
html[data-theme="light"] .form-group input,
html[data-theme="light"] .form-group textarea,
html[data-theme="light"] .form-group select,
html[data-theme="light"] .modal-content input,
html[data-theme="light"] .modal-content textarea,
html[data-theme="light"] .modal-box input,
html[data-theme="light"] .modal-box textarea,
html[data-theme="light"] .modal-box select,
html[data-theme="light"] .mod-input,
html[data-theme="light"] .mod-select,
html[data-theme="light"] .wh-select,
html[data-theme="light"] .search-box,
/* users.html paints the INPUT inside .search-box, not the wrapper, and it is
   caught by no other selector here - so it needs naming explicitly (2026-07-23,
   reported as "the Browse Users search bar is still black in light mode").
   The deeper cause of that report was the build stripping type="text" (see
   build.js removeRedundantAttributes); this line means the repair holds even
   if the attribute goes missing again. */
html[data-theme="light"] .search-box input,
html[data-theme="light"] .conv-search input,
html[data-theme="light"] .nm-search,
html[data-theme="light"] .chat-input-row textarea,
html[data-theme="light"] .msg-edit-form textarea,
html[data-theme="light"] .sidebar input,
html[data-theme="light"] .sidebar select,
html[data-theme="light"] .search-bar input,
html[data-theme="light"] .hero-search input,
html[data-theme="light"] .qd-input {
  background: var(--q-surface-2);
  color: var(--q-text);
  border-color: var(--q-border-strong);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] ::placeholder { color: var(--q-text-dim); opacity: 1; }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* The email / 2FA code boxes are styled with INLINE style attributes, which
   beat any stylesheet rule - !important is the only lever that reaches them.
   Light-scoped, so dark keeps its inline darks exactly. */
html[data-theme="light"] .email-code-digit {
  background: var(--q-surface-2) !important;
  color: var(--q-text) !important;
  border-color: var(--q-border-strong) !important;
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Autofilled fields: Chrome paints its own yellow unless it is overpainted by
   a 1000px inset shadow, which login/signup hard-code to #202225 - that stays
   a black slab in light until overridden here. */
html[data-theme="light"] input:-webkit-autofill,
html[data-theme="light"] input:-webkit-autofill:hover,
html[data-theme="light"] input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--q-text);
  -webkit-box-shadow: 0 0 0 1000px var(--q-surface-2) inset;
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Read-only fields (the auto-filled postcode) signal "not editable" with a
   DARKER fill in dark mode; in light that has to invert to a deeper tint, not
   simply carry the dark hex across. */
html[data-theme="light"] .sp-postcode input[readonly],
html[data-theme="light"] .edit-sp-pc input[readonly] {
  background: var(--q-well);
  color: var(--q-text-muted);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .sp-postcode input[readonly]:focus,
html[data-theme="light"] .edit-sp-pc input[readonly]:focus {
  border-color: var(--q-border-strong);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- ROLE: secondary / outline / ghost buttons ---------------------------
   The worst light-mode break, and the one in the reported screenshot: these
   fill #202225 and colour their label with var(--highlight), which light
   INVERTS to near-black. Result was near-black text on a near-black slab
   ("Message Owner", "View Profile"). */
html[data-theme="light"] .btn-secondary,
html[data-theme="light"] .btn-outline,
html[data-theme="light"] .btn-ghost,
html[data-theme="light"] .btn-cancel,
html[data-theme="light"] .back-btn,
html[data-theme="light"] .bc-btn-secondary,
html[data-theme="light"] .modaction-btn,
html[data-theme="light"] .change-username-btn,
html[data-theme="light"] .edit-bio-btn,
html[data-theme="light"] .action-btn.secondary,
html[data-theme="light"] .settings-link,
html[data-theme="light"] .reviews-filter-btn,
html[data-theme="light"] .btn-reset,
html[data-theme="light"] .msg-edit-actions button,
html[data-theme="light"] .qd-btn-secondary {
  background: var(--q-surface-2);
  color: var(--q-text);
  border-color: var(--q-border-strong);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .btn-secondary:hover,
html[data-theme="light"] .btn-outline:hover,
html[data-theme="light"] .btn-ghost:hover,
html[data-theme="light"] .btn-cancel:hover,
html[data-theme="light"] .back-btn:hover,
html[data-theme="light"] .bc-btn-secondary:hover,
html[data-theme="light"] .modaction-btn:hover,
html[data-theme="light"] .change-username-btn:hover,
html[data-theme="light"] .edit-bio-btn:hover,
html[data-theme="light"] .action-btn.secondary:hover,
html[data-theme="light"] .settings-link:hover,
html[data-theme="light"] .reviews-filter-btn:hover,
html[data-theme="light"] .btn-reset:hover,
html[data-theme="light"] .msg-edit-actions button:hover,
html[data-theme="light"] .qd-btn-secondary:hover {
  background: var(--q-hover);
  border-color: var(--q-accent);
  color: var(--q-text);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* profile.html's rename / edit-bio controls are ACCENT-TEXT buttons by design;
   the resting rule above would flatten their label to plain text colour, and
   the accent is the affordance marking them as actions rather than chrome.
   (account.html also tints its .btn-secondary accent, but that class is shared
   by seven pages and cannot be page-scoped from here - those Cancel/Change
   buttons simply pick up the site-standard secondary look, which is the more
   consistent result anyway.) */
html[data-theme="light"] .change-username-btn,
html[data-theme="light"] .edit-bio-btn { color: var(--q-accent-ink); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Primary buttons keep the accent fill (now the deepened light accent) - only
   restated where a page hard-codes the button's own colours. */
html[data-theme="light"] .msg-edit-actions button.primary,
html[data-theme="light"] .qd-btn-primary {
  background: var(--q-accent);
  color: #fff;
  border-color: var(--q-accent);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- ROLE: selectable chips / tiles (the 2px bordered boxes) -------------
   RESTING state only. Their selected state is re-stated below, because the
   resting rule here outranks the pages' own `.selected` / `.active`. */
html[data-theme="light"] .toggle-btn,
html[data-theme="light"] .hp-preset,
html[data-theme="light"] .hp-sel,
html[data-theme="light"] .day-label,
html[data-theme="light"] .edit-day,
html[data-theme="light"] .edit-rate-type,
html[data-theme="light"] .chooser-card,
html[data-theme="light"] .loc-opt,
html[data-theme="light"] .payment-option,
html[data-theme="light"] .qa-tile,
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* .q-choice = appearance.html's System/Light/Dark picker. Reported 2026-08-14:
   "the buttons on appearance are black and gross" in light. Cause: the chip
   shipped with v16.77's settings split and hard-codes background:#202225 /
   hover #2a2d31 in settings.css, but nobody added it to this role list, so it
   stayed a black slab on a white card. It IS a 2px selectable chip, so it
   belongs here with .qa-tile and friends rather than getting its own rule.
   DO NOT REVERT: removing this line puts the black buttons back. Tripwire -
   toggle to Light on appearance.html; all three chips must read pale grey. */
html[data-theme="light"] .q-choice,
html[data-theme="light"] .bc-next {
  background: var(--q-surface-2);
  color: var(--q-text);
  border-color: var(--q-border-strong);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .qa-tile:hover,
html[data-theme="light"] .loc-opt:hover,
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Re-stated because the resting repair above is (0,2,1) and settings.css's
   `.q-choice:hover` is only (0,2,0) - without this line hover is flattened. */
html[data-theme="light"] .q-choice:hover,
html[data-theme="light"] .chooser-card:hover { background: var(--q-hover); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .toggle-btn.active,
html[data-theme="light"] .hp-preset.active,
html[data-theme="light"] .hp-sel.selected,
html[data-theme="light"] .day-label.selected,
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ⚠ THESE TWO WERE WRITTEN AS `.selected` AND NOTHING EVER SETS `.selected` ON
   THEM - mylistings.html toggles `.on` (renderEditDays / renderEditRateTypes).
   So both were DEAD selectors while `.edit-day` and `.edit-rate-type` sat in
   the resting list above, which meant the edit-listing modal showed NO visible
   selection in light mode: the chosen days and the chosen rate type looked
   exactly like the unchosen ones. Found 2026-08-14 during the modal revamp.
   Corrected to `.on`. DO NOT "tidy" these back to `.selected` - grep the page
   for classList.toggle before changing a state class here.
   Third instance of this bug class - see [[project_light_selected_state_bug]]. */
html[data-theme="light"] .edit-day.on,
html[data-theme="light"] .edit-rate-type.on,
html[data-theme="light"] .payment-option.selected,
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* .loc-opt.active was MISSING from this list while .loc-opt sat in the resting
   list above - found 2026-08-14 while matching requests.html to list.html.
   Consequence: on list.html in LIGHT mode, "Where do you provide this service?"
   and the Fixed-rates/Quote toggle showed NO visible selection at all, because
   the resting repair (0,2,1) outranks the page's own .loc-opt.active (0,2,0).
   Exactly the flattening this block's header warns about. Now also used by
   requests.html's Type filter. DO NOT REVERT - tripwire: switch to Light,
   open list.html as a service, and confirm one location chip reads as chosen. */
html[data-theme="light"] .loc-opt.active,
html[data-theme="light"] .chooser-card.selected,
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Same re-statement rule as the hover above: settings.css's `.q-choice.selected`
   is (0,2,0) and loses to the (0,2,1) resting repair, so the accent-wash
   selected state has to be named here or the picker shows no selection at all
   in light. DO NOT REVERT (2026-08-14). */
html[data-theme="light"] .q-choice.selected,
html[data-theme="light"] .reviews-filter-btn.active {
  background: var(--q-accent-wash);
  border-color: var(--q-accent);
  color: var(--q-accent-ink);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ╔══════════════════════════════════════════════════════════════════════════╗
   ║ LIGHT-MODE CONTROL VISIBILITY (Rogan 2026-09-24) - DO NOT REVERT         ║
   ║ Resting fill: background: var(--q-surface-2)  (#eaedf4)                  ║
   ║ Hover fill:   background: var(--q-hover)      (#e2e6f0)                  ║
   ║ Active/current: restated with the page's own values, unchanged.          ║
   ╠══════════════════════════════════════════════════════════════════════════╣
   ║ THE BUG: Rogan's screenshot of index.html in light (the default). The    ║
   ║ filter sidebar's Type (Items, Services), Show only (Available today, Top ║
   ║ rated, Has reviews, Remote) and "Use my location" were white text-ish    ║
   ║ labels on a white panel: they did not look like buttons at all. Same for ║
   ║ the unselected half of the Listings / Requests switch on index.html and  ║
   ║ requests.html, and for the Quick actions on mycalendars.html.            ║
   ║ ROOT CAUSE: each resting control is painted with the SAME token as the   ║
   ║ card it sits on (index .chip = var(--card), mc-quick-link = var(--card), ║
   ║ browse-switch a = transparent on var(--fg)) and has no visible border.   ║
   ║ In dark those tokens differ (#202225 on #2f3136), so the control shows;  ║
   ║ in light both resolve to white (--q-surface) and the control vanishes.   ║
   ║ FIX: ONE treatment for all of them: the site's own "secondary control on ║
   ║ a card" fill, --q-surface-2 (the same fill inputs and .btn-secondary use ║
   ║ in light). A fill, not a border, because .browse-switch a and            ║
   ║ .mc-quick-link have no border and adding one would move them by 2px.    ║
   ║ These rules are (0,2,1) to (0,3,1): they OUTRANK the pages' own :hover / ║
   ║ .active / [aria-current] rules, so both states are RE-STATED below with  ║
   ║ the pages' exact values, or they would be flattened to the resting fill. ║
   ║ WHY SAFE: every selector starts html[data-theme="light"], so dark cannot ║
   ║ match (dark renders of index, requests, mycalendars were diffed before   ║
   ║ and after: pixel-identical). Page files are untouched.                   ║
   ║ HOW FOUND: every page rendered in headless Chrome, light, 1280 and 390,  ║
   ║ each control's fill and border measured against its container.          ║
   ║ Login-only pages rendered from their real shell templates, JS off.       ║
   ║ TRIPWIRE: open index.html in light; Items / Services / Available today   ║
   ║ must read as pale grey buttons on the white sidebar, the chosen one      ║
   ║ solid accent. scripts/tests/light-controls-test.mjs fails if any of      ║
   ║ these rules is removed. Listed in docs/ai/frontend.md.                   ║
   ╚══════════════════════════════════════════════════════════════════════════╝ */
/* index.html filter sidebar chips (Type, Show only, Use my location). Scoped to
   .sidebar: users.html also has a .chip, and it already shows (it is filled
   var(--background) on a white card), so it must not change. */
html[data-theme="light"] .sidebar .chip { background: var(--q-surface-2); }
html[data-theme="light"] .sidebar .chip:hover { background: var(--q-hover); }
html[data-theme="light"] .sidebar .chip.active { background: var(--accent); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* The Listings / Requests switch, index.html + requests.html (same rules on
   both pages). The current half is marked aria-current="page", not .active. */
html[data-theme="light"] .browse-switch a { background: var(--q-surface-2); }
html[data-theme="light"] .browse-switch a:hover { background: var(--q-hover); }
html[data-theme="light"] .browse-switch a[aria-current="page"],
html[data-theme="light"] .browse-switch a[aria-current="page"]:hover { background: var(--q-accent); color: #fff; }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* mycalendars.html sidebar Quick actions. No selected state; the page's hover
   is a solid accent fill with white text, restated exactly. */
html[data-theme="light"] .mc-quick-link { background: var(--q-surface-2); }
html[data-theme="light"] .mc-quick-link:hover { background: var(--q-accent); color: #fff; }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- ROLE: panels + cards the pages fill #202225 ------------------------- */
html[data-theme="light"] .listing-description,
html[data-theme="light"] .review-card-pl,
html[data-theme="light"] .reviews-summary,
html[data-theme="light"] .profile-bio,
html[data-theme="light"] .profile-email-box,
html[data-theme="light"] .reviewee-header,
html[data-theme="light"] .calendar-container,
html[data-theme="light"] .card-form,
html[data-theme="light"] .cal-cell,
html[data-theme="light"] .asset-card,
html[data-theme="light"] .shot-grid a,
html[data-theme="light"] .email-display,
html[data-theme="light"] .step-number,
html[data-theme="light"] .bc-summary,
html[data-theme="light"] details.faq {
  background: var(--q-surface-2);
  color: var(--q-text);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* .listing-card means TWO different things across the site: mycalendars and
   mylistings use it for the main card and paint it var(--foreground), which
   already themes to white - while review.html and user-profile hard-code
   #202225 for a small tile. Only the hard-coded pair is repainted, matched by
   context so the token-driven cards are left completely alone. */
html[data-theme="light"] .listings-grid .listing-card,
html[data-theme="light"] .listing-card:has(.listing-card-img) {
  background: var(--q-surface-2);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Panels that FLOAT above the page (dropdowns, popovers, action menus) have to
   read as lifted, so they take the white surface + border + shadow rather than
   the inset grey used for embedded panels. */
html[data-theme="light"] .edit-suburb-dd,
html[data-theme="light"] .card-menu-list,
html[data-theme="light"] .msg-action-menu {
  background: var(--q-surface);
  color: var(--q-text);
  border: 1px solid var(--q-border);
  box-shadow: var(--q-shadow-pop);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* The category / address autocompletes deliberately wear an ACCENT border to
   tie them to the focused input - keep that, only the fill needs repainting. */
html[data-theme="light"] .address-suggestions,
html[data-theme="light"] .cat-dd {
  background: var(--q-surface);
  color: var(--q-text);
  border-color: var(--q-accent);
  box-shadow: var(--q-shadow-pop);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* v16.92 (2026-08-25): the category dropdown gained three NON-pickable row
   types - a refusal banner, a group heading and the switch-taxonomy row (see
   the block beside .cat-item in list.html). Their dark styling uses
   white-alpha tints and #2a2d31 rules, both of which vanish on a white
   surface, so each needs a light-side repaint here. .cat-item itself is
   already handled further down with the other bordered rows.
   ⚠ .cat-switch-go must stay ACCENT-ON-WASH, not accent-on-accent - that is
   the exact mistake the desktop rail hover made (see js/shell.mjs). */
html[data-theme="light"] .cat-note {
  background: var(--q-danger-wash);
  border-bottom-color: var(--q-border);
}
html[data-theme="light"] .cat-note strong { color: var(--q-text); }
html[data-theme="light"] .cat-note span   { color: var(--q-text-muted); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .cat-group       { color: var(--q-text-muted); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .cat-switch {
  background: var(--q-accent-wash);
  border-top-color: var(--q-border);
}
html[data-theme="light"] .cat-switch:hover  { background: var(--q-hover); }
html[data-theme="light"] .cat-switch strong { color: var(--q-text); }
html[data-theme="light"] .cat-switch span   { color: var(--q-text-muted); }
html[data-theme="light"] .cat-switch .cat-switch-go { color: var(--q-accent-ink); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* v16.93 (2026-08-26): the item safety notice on list.html (#hireNoticeGroup).
   Its dark form is a white-alpha wash, which disappears on a white surface, so
   the fill has to be repainted here. The accent left rule already resolves via
   --primary-accent and needs no override. Same wash as .cat-switch above so
   the two callouts on that one form read as the same weight of thing. */
html[data-theme="light"] .hire-notice        { background: var(--q-accent-wash); color: var(--q-text-muted); }
html[data-theme="light"] .hire-notice strong { color: var(--q-text); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- ROLE: image wells + skeletons (the #1a1c1f / #191b1e backdrops) ------
   These sit BEHIND a photo (the ambient-backdrop pattern) or stand in for one
   while it loads, so they must stay a flat tint. This is the block that was
   previously "fixed" by editing index.html, which is what turned the dark
   skeletons into pale bare squares. Repaired light-side only. */
html[data-theme="light"] .listing-image,
html[data-theme="light"] .listing-img,
html[data-theme="light"] .listing-card-img,
html[data-theme="light"] .main-image,
html[data-theme="light"] .thumbnail,
html[data-theme="light"] .image-preview-item,
html[data-theme="light"] .edit-image-thumb,
html[data-theme="light"] .listing-embed-image,
html[data-theme="light"] .be-thumb,
html[data-theme="light"] .notif-thumb,
html[data-theme="light"] .bc-summary-img,
html[data-theme="light"] .asset-card img,
html[data-theme="light"] .skel-img,
html[data-theme="light"] .skel-ln,
html[data-theme="light"] .ci            { background: var(--q-well); }
html[data-theme="light"] .ci .ni        { color: var(--q-text-muted); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- ROLE: inset wells that hold controls -------------------------------- */
/* v16.73 (2026-08-07): .qty-picker added. REPORTED BY ROGAN with a screenshot -
   "How many do you need?" rendered as a full BLACK box in light mode on
   calendars.html. It hard-codes `background:#202225` with a #3a3d42 border and
   was simply never listed here, so nothing repainted it. Its text already used
   --highlight / --secondary-accent, which ARE remapped in light, so the label
   was dark-on-dark-grey and the whole panel looked like a hole in the page.
   It belongs in this group because it is the same shape as .hourly-panel: an
   inset well on the same page holding a .num-wrap stepper.
   DO NOT REMOVE. Dark is untouched by construction (this block cannot match
   unless data-theme="light"). Swept the whole site the same day: the only
   other raw-hex dark backgrounds left unthemed were .pg-ell:hover (added
   below) and two DELIBERATE ones - .day-tip (#111 tooltip, correct dark in
   both themes) and .status-pill.completed .status-dot (#5865F2 blurple). */
html[data-theme="light"] .hourly-panel,
html[data-theme="light"] .stripe-field,
html[data-theme="light"] .msg-edit-form,
html[data-theme="light"] .qd-cal,
html[data-theme="light"] .qty-picker,
html[data-theme="light"] .toggle-switch {
  background: var(--q-well);
  border-color: var(--q-border-strong);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- ROLE: hairline dividers drawn in #1a1c1f / #2a2d31 / #333 -----------
   A near-black rule on a white card is a hard black line; soften to the light
   border token. Only the COLOUR is touched, never the width or side. */
html[data-theme="light"] .setting-row,
html[data-theme="light"] .info-row,
html[data-theme="light"] .activity-item,
html[data-theme="light"] .summary-row,
html[data-theme="light"] .form-section,
html[data-theme="light"] .price-row,
html[data-theme="light"] .address-suggestion-item,
html[data-theme="light"] .cat-item,
html[data-theme="light"] .edit-suburb-item,
html[data-theme="light"] .listing-meta,
html[data-theme="light"] .availability-section,
html[data-theme="light"] .listing-stars-section,
html[data-theme="light"] .modal-section + .modal-section,
html[data-theme="light"] .chat-panel-header,
html[data-theme="light"] .chat-top-bar,
html[data-theme="light"] .notif-panel-header,
html[data-theme="light"] .hourly-panel-header,
html[data-theme="light"] .be-view,
html[data-theme="light"] .cal-nav button,
html[data-theme="light"] .user-stats,
html[data-theme="light"] table.data th,
html[data-theme="light"] table.data td,
html[data-theme="light"] .fact-table td,
html[data-theme="light"] .compare-table td,
html[data-theme="light"] table.compare th,
html[data-theme="light"] table.compare td,
html[data-theme="light"] .edit-image-thumb,
html[data-theme="light"] .pg-num:hover,
html[data-theme="light"] .pg-arrow:not([disabled]):hover,
html[data-theme="light"] .edit-file-row input[type="file"]::file-selector-button,
html[data-theme="light"] .cf { border-color: var(--q-border); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Divider rules drawn as filled elements / pseudo-elements, not as borders. */
html[data-theme="light"] .auth-divider::before,
html[data-theme="light"] .auth-divider::after,
html[data-theme="light"] .date-divider::before,
html[data-theme="light"] .date-divider::after,
html[data-theme="light"] .step-divider,
html[data-theme="light"] .card-menu-list .menu-divider,
html[data-theme="light"] .profile-menu .divider { background: var(--q-border); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- ROLE: control fills that are NOT buttons-with-a-border --------------
   Small square/round controls the pages fill with a flat dark. The month
   steppers on the availability calendar landed here: they were repaired for
   border only, so they kept `background:#202225` and rendered as two black
   squares on the white card (reported). */
html[data-theme="light"] .cal-nav button,
html[data-theme="light"] .calendar-nav button {
  background: var(--q-surface-2);
  color: var(--q-text);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* The calendar legend's "Unavailable" swatch is an INLINE style, so it needs
   both !important and a value-matched selector. It must track .cal-day.past,
   which the dim-text rule below repaints - a legend that disagrees with the
   grid it explains is worse than no legend. */
html[data-theme="light"] .cal-legend-dot[style*="#3a3d42"] {
  background: var(--q-text-dim) !important;
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ── v16.83 (2026-08-14) EVERYTHING CALENDAR: the one light repair it needs ──
   mycalendars.html's new .mccal-* month grid is written in tokens only, so it
   themes itself - with ONE exception, caught in a render harness before it
   shipped rather than by Rogan on a screenshot.

   THE COLLISION: the grid card is var(--card) and its day cells are
   var(--foreground). In DARK those are two different greys (#202225 card,
   #2c2f33 cell) and the cells read as raised tiles. In LIGHT both tokens
   remap to #ffffff - --card to --q-surface and --foreground to --q-surface
   as well - so 42 white cells sat on a white card and the entire grid
   vanished. Same shape of bug as the .qty-picker black box, in reverse:
   there a hex stayed dark, here two tokens collapse onto one value.

   FIX: cells and the month arrows take --q-surface-2, the "sits ON a card"
   step. That is what the .cal-nav button repair above already does, and it
   keeps the intended depth order in light: surface (card) > surface-2 (cell)
   > well (past cell) > hover.

   ⚠ THE THREE STATE LINES BELOW ARE NOT OPTIONAL PADDING - THEY ARE THE
   SPECIFICITY REPAIR. `html[data-theme="light"] .mccal-cell` scores (0,2,1);
   the page's own `.mccal-cell.past` / `.sel` / `.blank` score (0,2,0). The
   base rule therefore OUTRANKS every state the page defines, and without
   these three restatements a light-mode grid would paint past days, the
   selected day and the empty leading/trailing cells all in flat --q-surface-2:
   no past shading and, worse, no visible selected day at all. That is the
   exact failure in [[project_light_selected_state_bug]] - a chip present in
   the resting list and missing from the selected list - which has already
   bitten .q-choice and .loc-opt. Add a new .mccal-cell state and you must add
   it HERE in the same edit.
   DO NOT REMOVE. Dark cannot be touched by this block by construction.
   TRIPWIRE: switch Appearance to Light and open My Calendars. Day cells must
   have visible edges against the card, past days must read greyer, and the
   day you tap must be accent-washed. */
html[data-theme="light"] .mccal-cell,
html[data-theme="light"] .mccal-nav button { background: var(--q-surface-2); }
html[data-theme="light"] .mccal-nav button:hover { background: var(--q-hover); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* blank = padding, not a day. Must stay see-through in both themes. */
html[data-theme="light"] .mccal-cell.blank { background: transparent; }
html[data-theme="light"] .mccal-cell.past  { background: var(--q-well); }
html[data-theme="light"] .mccal-cell.sel   { background: var(--q-accent-wash); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- ROLE: hover fills (#35383d / #34383d / #2a2d31 raise-on-hover) ------ */
html[data-theme="light"] .calendar-nav button:hover,
html[data-theme="light"] .cal-nav button:hover,
html[data-theme="light"] .hourly-panel-close:hover,
html[data-theme="light"] .notif-row.is-new:hover,
html[data-theme="light"] .pg-num:hover,
html[data-theme="light"] .pg-arrow:not([disabled]):hover,
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* v16.73 (2026-08-07): .pg-ell:hover was the one member of the pagination trio
   that never got listed, so hovering the "..." flashed a #34383d near-black
   patch between two correctly-themed light buttons. Found by sweeping every
   page for raw-hex dark backgrounds with no repair here. index.html and
   users.html both carry it. DO NOT REMOVE - keep the three in step. */
html[data-theme="light"] .pg-ell:hover,
html[data-theme="light"] .cal-day.past-booked { background: var(--q-hover); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Unread notification rows carry a standing tint (not just on hover). */
html[data-theme="light"] .notif-row.is-new { background: var(--q-accent-wash); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- ROLE: dim / disabled text (#555, #444, #3a3d42 used as a COLOUR) -----
   In dark these read as "barely lighter than the background". In light they
   have to invert to "barely darker", not carry the dark hex across. */
html[data-theme="light"] .calendar-day.disabled,
html[data-theme="light"] .calendar-day.not-listed,
html[data-theme="light"] .cal-day.past,
html[data-theme="light"] .cal-day.off,
html[data-theme="light"] .sna,
html[data-theme="light"] .star,
html[data-theme="light"] .qd-cal-day.past { color: var(--q-text-dim); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .calendar-day.disabled,
html[data-theme="light"] .calendar-day.not-listed,
html[data-theme="light"] .cal-day.past,
html[data-theme="light"] .cal-day.off,
html[data-theme="light"] .qd-cal-day.past,
html[data-theme="light"] .qd-cal-day.past:hover { background: var(--q-well); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* v16.51 (2026-07-25, Rogan): the booking calendar's past / not-listed cells sat
   on --q-well - almost the same tint as the .calendar-container behind them - so
   the grid read as a flat, near-invisible wash in light mode. Nudge JUST the
   booking-calendar cells a touch grayer for legibility; other pages' calendars
   keep --q-well. Later in source with equal specificity, so it wins for these. */
html[data-theme="light"] .calendar-day.disabled,
html[data-theme="light"] .calendar-day.not-listed { background: #d6dbe4; }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Filled stars must keep the gold: the blanket `.star` rule above outranks the
   page's own (0,2,0) `.star.filled`, so it has to be restated at this level. */
html[data-theme="light"] .star.filled { color: var(--q-gold); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Thumbnail selection ring follows the light accent. */
html[data-theme="light"] .thumbnail:hover,
html[data-theme="light"] .thumbnail.active { border-color: var(--q-accent); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ---------------------------------------------------------------------------
   CHROME + CONTROLS per theme (scrollbars, number steppers).
   13 pages hard-code dark scrollbars via `*` selectors, and pages reuse --hd
   (the header colour) as a CONTROL fill - in light --hd is WHITE, so steppers
   would render white-on-white and scrollbars would stay dark. These selectors
   are deliberately more specific than the page rules they override:
     html[data-theme=...] *              (0,1,1) beats  *                  (0,0,0)
     html[data-theme=...] .num-spin button (0,2,2) beats .num-spin button  (0,1,1)
   NOTE the :hover override below is REQUIRED: (0,2,2) would otherwise also beat
   the pages' own `.num-spin button:hover` (0,2,1) and kill the hover state.
   --------------------------------------------------------------------------- */
html[data-theme="light"] { --q-scroll-thumb: #b6bccb; --q-scroll-track: #e6e9f1; }

/* LIGHT ONLY. Dark deliberately has NO rules here: pages already style their
   own scrollbars and 20 of them never had thin scrollbars at all, so a dark
   rule here would silently restyle them. Dark must render byte-identical to
   what shipped. */
html[data-theme="light"], html[data-theme="light"] * {
  scrollbar-width: thin;
  scrollbar-color: var(--q-scroll-thumb) var(--q-scroll-track);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] ::-webkit-scrollbar-track { background: var(--q-scroll-track); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] ::-webkit-scrollbar-thumb {
  background: var(--q-scroll-thumb); border-radius: 3px;
}
html[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: #9aa2b4; }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Number steppers (the up/down buttons beside quantity/price inputs). */
html[data-theme="light"] .num-spin button {
  background: var(--q-surface-2);
  color: var(--q-text-muted);
  border-color: var(--q-border);
}
html[data-theme="light"] .num-spin button:hover {
  background: var(--q-accent);
  color: #fff;
  border-color: var(--q-accent);
}

/* ---------------------------------------------------------------------------
   LIGHT REPAIRS FOR RUNTIME-INJECTED CHROME (auth-ui.mjs / dialog.mjs)
   ---------------------------------------------------------------------------
   Those modules append a <style> to <head> at load, i.e. AFTER this file, so
   they win any tie on source order - but they use plain class selectors
   (0,1,0)/(0,2,0), and `html[data-theme="light"] .x` is (0,2,1)/(0,3,1), so
   specificity still carries these. The alternative was theming inside the JS
   template literals, which would put dark values one typo away from a live
   edit; keeping it here means the JS is never touched.
   --------------------------------------------------------------------------- */

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Avatar dropdown (the menu in the reported screenshot: a black panel hanging
   off a white header). It is a floating surface, so: white, hairline, shadow. */
html[data-theme="light"] .profile-menu {
  background: var(--q-surface);
  border: 1px solid var(--q-border);
  box-shadow: var(--q-shadow-pop);
}
html[data-theme="light"] .profile-menu a,
html[data-theme="light"] .profile-menu button { color: var(--q-text-muted); }
html[data-theme="light"] .profile-menu a:hover,
html[data-theme="light"] .profile-menu button:hover {
  background: var(--q-hover);
  color: var(--q-text);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Avatar button: a dark gradient puck that shows while the image loads (and
   permanently for users with no picture). */
html[data-theme="light"] .profile-btn {
  background: linear-gradient(180deg, var(--q-surface-2), var(--q-well));
  box-shadow: 0 1px 4px rgba(20,22,28,0.14);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Header icon button - `color:#fff` + a white-alpha hover are both invisible
   against a white bar. */
html[data-theme="light"] .inbox-btn { color: var(--q-text-muted); }
html[data-theme="light"] .inbox-btn:hover {
  color: var(--q-text);
  background: var(--q-hover);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Logged-out header buttons: `.ghost` is white text on a white bar. */
html[data-theme="light"] .auth-buttons button.ghost {
  color: var(--q-text);
  border-color: var(--q-border-strong);
}
html[data-theme="light"] .auth-buttons button.ghost:hover { background: var(--q-hover); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Count badges are ringed in the HEADER colour so they punch out of the bar;
   that ring is hard-coded #1a1c1f and would draw a black halo on white. */
html[data-theme="light"] .inbox-badge,
html[data-theme="light"] .mycal-badge { box-shadow: 0 0 0 2px var(--q-header); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* dialog.mjs modals - the site-standard confirm/prompt surface. */
html[data-theme="light"] .qd-box {
  background: var(--q-surface);
  color: var(--q-text-muted);
  box-shadow: var(--q-shadow-pop);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .qd-title,
html[data-theme="light"] .qd-label,
html[data-theme="light"] .qd-cal-title { color: var(--q-text); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .qd-msg,
html[data-theme="light"] .qd-note,
html[data-theme="light"] .qd-cal-hd,
html[data-theme="light"] .qd-cal-picked { color: var(--q-text-muted); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .qd-overlay { background: rgba(20,22,28,0.45); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .qd-cal-day {
  background: var(--q-surface-2);
  color: var(--q-text);
}
html[data-theme="light"] .qd-cal-day:hover { background: var(--q-hover); }
html[data-theme="light"] .qd-cal-day.off,
html[data-theme="light"] .qd-cal-day.off:hover { background: transparent; }
html[data-theme="light"] .qd-cal-day.sel,
html[data-theme="light"] .qd-cal-day.in-range { color: #fff; }
html[data-theme="light"] .qd-cal-day.sel { background: var(--q-accent); }
html[data-theme="light"] .qd-cal-day.in-range { background: rgba(91,104,204,0.35); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .qd-cal-nav button {
  background: var(--q-surface-2);
  border-color: var(--q-border-strong);
  color: var(--q-text-muted);
}
html[data-theme="light"] .qd-cal-nav button:hover:not(:disabled) {
  color: var(--q-text);
  border-color: var(--q-accent);
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* The two full-screen blocking gates (2FA step-up, policy re-consent) are
   built entirely from INLINE style attributes in auth-ui.mjs, so nothing but
   !important reaches them - they would otherwise stay a full dark screen in
   front of a light site. Only the surfaces and text are repainted; the accent
   buttons and the red error text are already correct on either theme. */
html[data-theme="light"] #q-mfa-gate,
html[data-theme="light"] #q-policy-gate { background: var(--q-bg) !important; }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] #q-mfa-gate > div,
html[data-theme="light"] #q-policy-gate > div {
  background: var(--q-surface) !important;
  box-shadow: var(--q-shadow-pop) !important;
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] #q-mfa-gate h2,
html[data-theme="light"] #q-policy-gate h2 { color: var(--q-text) !important; }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] #q-mfa-gate p,
html[data-theme="light"] #q-mfa-gate label,
html[data-theme="light"] #q-policy-gate p { color: var(--q-text-muted) !important; }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] #q-mfa-code,
html[data-theme="light"] #q-mfa-rec-code {
  background: var(--q-surface-2) !important;
  color: var(--q-text) !important;
  border-color: var(--q-border-strong) !important;
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] #q-mfa-recovery-link {
  background: var(--q-surface-2) !important;
  color: var(--q-text) !important;
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] #q-mfa-logout,
html[data-theme="light"] #q-mfa-back,
html[data-theme="light"] #q-policy-logout { color: var(--q-text-muted) !important; }

/* v16.92 (2026-08-25): the light-mode .q-drawer overrides that sat here are
   gone with the drawer itself (Rogan: "Hamburger menu still exists in the
   background. Eradicate it."). Nothing else in the project uses .q-drawer -
   grepped across css, js and all 42 pages. */

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ---------------------------------------------------------------------------
   LIGHT REPAIRS: messages.html
   Chat is the densest dark-assuming surface on the site - bubbles, embeds and
   notification rows all stack #2a2d31 on #202225 on #1a1c1f, which is three
   shades of "dark" carrying meaning. Light rebuilds that ladder upward instead
   (raised bubble > page) rather than mapping each hex to a lighter one.
   --------------------------------------------------------------------------- */
html[data-theme="light"] .msg.received .msg-bubble {
  background: var(--q-raised);
  color: var(--q-text);
  border: 1px solid var(--q-border);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .msg-bubble.is-deleted { border-color: var(--q-border-strong); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .msg-system .msg-bubble {
  background: var(--q-accent-wash);
  color: var(--q-text-muted);
  border-color: var(--q-border);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .listing-embed,
html[data-theme="light"] .booking-embed {
  background: var(--q-surface);
  border-color: var(--q-border);
  box-shadow: var(--q-shadow);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .listing-embed:hover,
html[data-theme="light"] .booking-embed:hover { border-color: var(--q-accent); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .be-view { background: var(--q-surface-2); color: var(--q-text); }
html[data-theme="light"] .be-view:hover { background: var(--q-hover); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Conversation + notification rows. #25282c is a "one shade up from the panel"
   fill in dark; in light the same idea has to run the other way, and the ACTIVE
   conversation additionally carries an accent border, so it takes the accent
   wash rather than the plain hover fill. */
html[data-theme="light"] .conv-item.active { background: var(--q-accent-wash); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .notif-row:hover  { background: var(--q-hover); }
html[data-theme="light"] .notif-row.status-muted .notif-status { color: var(--q-text-dim); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* --- Remaining page-local darks that share no vocabulary with the above --- */
html[data-theme="light"] .mod-chip-row { background: var(--q-surface-2); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .sidebar-close { border-color: var(--q-border-strong); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* .review-card is TWO different things: user-profile's small review tiles
   (hard-coded #202225) and review.html's main form card (var(--foreground),
   which already themes to white). Scoped to the reviews panel so repainting
   the tiles cannot grey out review.html's card. */
html[data-theme="light"] .reviews-panel .review-card { background: var(--q-surface-2); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* signup's confirmation line is a near-neon green tuned for a dark card. */
html[data-theme="light"] .success { color: var(--q-success); }

/* ═══════════════════════════════════════════════════════════════════════════
   ║ v16.54 SWEEP (2026-07-27, Rogan) - DO NOT REVERT                        ║
   ║                                                                         ║
   ║ BUG: "the default profile picture top-right is still dark in light      ║
   ║ mode - we KEEP seeing more of these pop up". Reported with a screenshot ║
   ║ of the light-mode header showing a black avatar coin.                   ║
   ║                                                                         ║
   ║ ROOT CAUSE: the repair sheet above only reaches values that live in a   ║
   ║ CSS DECLARATION. A full re-audit of all 33 pages + every JS module      ║
   ║ (luminance-scored every colour literal, then cross-referenced against   ║
   ║ the selectors already named above) found the survivors are all in the   ║
   ║ FOUR places a declaration-level repair structurally cannot see:         ║
   ║   1. SVG presentation ATTRIBUTES  (fill="#111" baked into a JS template)║
   ║   2. CSS injected by a JS module that shipped AFTER this sheet was      ║
   ║      written (mock-payments.mjs)                                        ║
   ║   3. page classes added after this sheet was written (v16.51 edit modal)║
   ║   4. states this sheet's own !important flattened (see .email-code-digit║
   ║      below - the sheet caused that one itself)                          ║
   ║ The TOKEN layer is clean: every --custom-prop declared in any page      ║
   ║ :root is remapped by the html[data-theme="light"] block at the top of   ║
   ║ this file, verified programmatically. So this is the tail, not a sample.║
   ║                                                                         ║
   ║ WHY SAFE: every rule below is scoped to html[data-theme="light"], which ║
   ║ is structurally incapable of matching when dark is active - dark stays  ║
   ║ byte-identical. NOTHING in the pages or the JS modules was edited, so   ║
   ║ the dark values stay exactly where they shipped.                        ║
   ║                                                                         ║
   ║ TRIPWIRE: if a black avatar coin, charcoal empty stars, invisible card- ║
   ║ number text, or hard black hairlines in the edit modal reappear in      ║
   ║ light mode, this block was deleted or its scope prefix was dropped.     ║
   ═══════════════════════════════════════════════════════════════════════════ */

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* 1. THE REPORTED BUG. auth-ui.mjs builds the header's no-photo avatar from a
   template literal whose disc is `fill="#111"` with a pale-grey figure on top -
   correct on a dark bar, a black coin on a white one. Presentation attributes
   lose to ANY css rule, so this is reachable from here with no !important and
   without touching the JS (which is the rule for runtime-injected chrome).
   The disc goes transparent so the .profile-btn gradient repaired above shows
   through as the puck, and the figure inverts from pale to muted grey - the
   same "well + muted figure" pairing .ci / .ni already use for a missing image. */
html[data-theme="light"] .profile-btn > svg > circle { fill: transparent; }
html[data-theme="light"] .profile-btn > svg g circle,
html[data-theme="light"] .profile-btn > svg g path { fill: var(--q-text-muted); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* 2. Empty + half-filled rating stars. index.html, publiclistings.html and
   user-profile.html each carry their own copy of a starSVG() helper that paints
   the EMPTY half #3a3d42 (as a solid fill, as an outline stroke, and as the
   second stop of the half-star gradient). On white that is a charcoal star that
   competes with the gold filled ones instead of receding behind them.
   Matched BY VALUE rather than by container, because the three helpers sit in
   three different page structures and a fourth page copying the helper should
   theme for free - same tactic as the .cal-legend-dot[style*=] repair above.
   #3a3d42 appears as a fill/stroke/stop-color ATTRIBUTE nowhere else in src.
   --q-text-dim keeps these in step with the css-glyph stars (`.star`, repaired
   above), which are the same UI element rendered a different way. */
html[data-theme="light"] svg path[fill="#3a3d42"]      { fill: var(--q-text-dim); }
html[data-theme="light"] svg path[stroke="#3a3d42"]    { stroke: var(--q-text-dim); }
html[data-theme="light"] svg stop[stop-color="#3a3d42"]{ stop-color: var(--q-text-dim); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* 3. mock-payments.mjs injects `.mock-card-input { color:#ffffff }` for the fake
   card fields. Those mount INSIDE .stripe-field, which this sheet repaints to
   --q-well - so in light the card number, expiry and CVC were white on a pale
   grey well, i.e. invisible while typing. This is on the live booking path
   right now (Stripe is paused and mocked), so it is a real break, not a
   pre-launch nicety. Placeholder already themes via the global ::placeholder
   rule above, which outranks .mock-card-input::placeholder. */
html[data-theme="light"] .mock-card-input { color: var(--q-text); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* 4. v16.51 edit-listing modal (mylistings.html). Its sticky header, sticky
   footer and section rules were drawn in #3a3d42 / #34373c - fine as a subtle
   step on a dark card, hard black lines across a white one. Colour only; the
   widths, sides and the sticky layout are untouched. */
html[data-theme="light"] .edit-modal-head,
html[data-theme="light"] .edit-modal-content > .modal-buttons,
html[data-theme="light"] .edit-section + .edit-section { border-color: var(--q-border); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* 5. reports.html's Cancel button hard-codes `border:1px solid #444` as an
   INLINE style, so only !important reaches it.
   Matched STRUCTURALLY, not by [style*="#444"]: that button is emitted from a
   JS template literal, and build.js runs the inline <script> through the
   obfuscator, which splits and hex-escapes string literals - so a text match on
   the source would hold in src/ and silently miss in public/. (The runtime DOM
   value is intact either way, but there is no reason to depend on that here
   when the markup gives us a stable hook.) .btn-row holds exactly two buttons,
   Cancel and .btn-primary, so :not() isolates the Cancel one. */
html[data-theme="light"] .btn-row .btn:not(.btn-primary) {
  border-color: var(--q-border-strong) !important;
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* 6. auth-ui.mjs also rings the scrollbar thumb in #1a1c1f and fills the corner
   (where a vertical and horizontal bar meet) with it. The track/thumb repair
   above missed both, leaving a black hairline around the thumb and a black
   square in the bottom-right corner of scrollable panels. */
html[data-theme="light"] ::-webkit-scrollbar-thumb { border-color: var(--q-scroll-track); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] ::-webkit-scrollbar-corner { background: var(--q-scroll-track); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* 7. Self-inflicted: the .email-code-digit repair near the top of this sheet
   uses !important (it has to - the digits are styled inline), which also beat
   the focus colour account.html sets via input.style.borderColor. Net effect in
   light was a code field with NO focus affordance at all. Restated at higher
   specificity so focus wins again, per the "re-state any state a repair covers"
   note in this sheet's header. */
html[data-theme="light"] .email-code-digit:focus { border-color: var(--q-accent) !important; }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* 8. profile.html insets each unread moderation warning on rgba(0,0,0,0.18) -
   a "one step darker" wash that has to run the other way on a light amber
   banner, or the row reads as a muddy grey-brown slab. Inline, so !important.
   Matched by STRUCTURE for the same reason as 5 above (this row is built in a
   JS template literal that the build obfuscates): the warning row is the only
   child of the banner that holds an "I understand" ack button. :has() is
   already used by this sheet (see the .listing-card repair). */
html[data-theme="light"] .admin-warnings-banner div:has(> button[data-ack-warning]) {
  background: rgba(255,255,255,0.6) !important;
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
:root {
  /* Fluid type scale: clamp(min, preferred, max). Base ~1rem. */
  --q-text-xs:   clamp(0.72rem, 0.69rem + 0.15vw, 0.80rem);
  --q-text-sm:   clamp(0.82rem, 0.78rem + 0.20vw, 0.92rem);
  --q-text-base: clamp(0.95rem, 0.90rem + 0.25vw, 1.05rem);
  --q-text-md:   clamp(1.05rem, 0.98rem + 0.40vw, 1.25rem);
  --q-text-lg:   clamp(1.25rem, 1.10rem + 0.70vw, 1.60rem);
  --q-text-xl:   clamp(1.50rem, 1.25rem + 1.20vw, 2.20rem);
  --q-text-2xl:  clamp(1.90rem, 1.50rem + 2.00vw, 3.00rem);

  /* Fluid spacing scale. */
  --q-space-1: clamp(0.40rem, 0.35rem + 0.20vw, 0.60rem);
  --q-space-2: clamp(0.60rem, 0.50rem + 0.40vw, 1.00rem);
  --q-space-3: clamp(0.90rem, 0.70rem + 0.80vw, 1.60rem);
  --q-space-4: clamp(1.30rem, 1.00rem + 1.40vw, 2.40rem);
  --q-space-5: clamp(1.80rem, 1.30rem + 2.40vw, 3.50rem);

  /* Layout primitives. */
  --q-maxw:        1200px;  /* default content cap */
  --q-maxw-narrow:  640px;  /* forms / auth / article column */
  --q-radius:        12px;
  --q-gutter: clamp(1rem, 4vw, 2.5rem);  /* fluid page side padding */
}

/* ---- Safe global rules (the only non-opt-in part) ---------------------- */

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Stop iOS Safari from auto-inflating text in landscape / after orientation. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Media never forces horizontal scroll on a narrow screen. Only constrains
   when an element would otherwise overflow its container, so fixed-size
   avatars/icons in wider containers are unaffected. */
img, video { max-width: 100%; }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Form controls that are width:100% should include their padding in that
   width. Without this, on the pages that have no global box-sizing reset, a
   width:100% input overflows its card by the padding amount (a common cause
   of inputs spilling past the card edge on mobile). Scoped to form controls
   only - it can't shift container/box layouts tuned for content-box. */
input, textarea, select { box-sizing: border-box; }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Footer link rows wrap as whole items: a multi-word link like "Terms of
   Service" must never split across two lines on a narrow screen (the messy
   wrap seen on ~430px phones). The row still wraps between items. */
footer a { white-space: nowrap; }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ── Mobile footer normalisation (2026-07-23) - DO NOT REVERT ───────────────
   BUG: on a phone (~393px CSS wide) the footer on contact.html and ~28 other
   pages rendered noticeably WIDER than the rest of the page - the centred
   "(c) 2026 QRAUZ . Serving Australia . Guides . Contact ..." row ran hard
   into both screen edges and wrapped badly, while index.html's footer sat
   neatly inset. Reported with screenshots 2026-07-23.
   ROOT CAUSE: two things, neither of them a page-width problem.
     1. Those pages declare `footer { padding: 2rem 0; font-size: 0.9rem; }` -
        ZERO horizontal padding, so the text box is the full viewport width.
     2. Many of them also carry `footer a { margin: 0 0.7rem; }`, adding
        ~22px around every link, which pushes the row past the viewport.
   index.html, account.html and calendars.html already had the tuned
   @media (max-width:480px) rule below (padding 1.5rem 1rem / 0.85rem) and
   looked correct - it was simply never copied into the other pages.
   FIX: declare it once, here, instead of hand-patching 28 page <style>
   blocks. Page <style> blocks are parsed AFTER this stylesheet, so a bare
   `footer {...}` selector here would LOSE the cascade to their own
   `footer {...}` rules (media queries add no specificity). `html footer` is
   (0,0,2) and beats them, and unlike `body > footer` it still matches
   reset-password.html where the footer is nested inside #pageWrap.
   `box-sizing: border-box` is required because this project has no global
   box-sizing reset (deliberate - see the note at the top of this file) and
   several of these footers are `width: 100%`; without it the new 1rem side
   padding would push them 32px past the viewport and create the horizontal
   overflow we are trying to remove.
   SAFE: scoped to <=480px and to <footer> only. Desktop and tablet are
   byte-identical. The values are copied verbatim from index.html's existing
   rule and every page ships the same footer markup (links separated by
   literal " . " characters, so margin:0 loses no spacing), which is exactly
   how index/account/calendars already render - those three are unchanged.
   TRIPWIRE: if a phone footer starts touching both screen edges again, or a
   page's own footer padding/font-size starts winning below 480px, this block
   was deleted or its `html ` specificity prefix was dropped. */
@media (max-width: 480px) {
  html footer { padding: 1.5rem 1rem; font-size: 0.85rem; box-sizing: border-box; }
  html footer a { margin: 0; }
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Search inputs: replace the browser's default clear "x" with the brand masked
   cross (the one index.html's search bar already uses), site-wide. Cosmetic
   only - it restyles nothing but the native search-cancel button, so there is
   no layout risk. WebKit/Blink only (Firefox renders no cancel button), which
   matches the existing index.html behaviour. Colours use the cross-page
   fallback chain (see colour note above): muted normal, bright on hover. */
input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none; appearance: none;
  height: 16px; width: 16px; margin-left: 0.5rem; cursor: pointer;
  background-color: var(--muted, var(--secondary-accent, #99aab5));
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cline x1='6' y1='6' x2='18' y2='18' stroke='%23000' stroke-width='2.6' stroke-linecap='round'/%3E%3Cline x1='18' y1='6' x2='6' y2='18' stroke='%23000' stroke-width='2.6' stroke-linecap='round'/%3E%3C/svg%3E") center/contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cline x1='6' y1='6' x2='18' y2='18' stroke='%23000' stroke-width='2.6' stroke-linecap='round'/%3E%3Cline x1='18' y1='6' x2='6' y2='18' stroke='%23000' stroke-width='2.6' stroke-linecap='round'/%3E%3C/svg%3E") center/contain no-repeat;
  transition: background-color 0.12s;
}
input[type="search"]::-webkit-search-cancel-button:hover {
  background-color: var(--white, var(--highlight, #ffffff));
}

/* ---- Brand lockup outline (header) -------------------------------------- */

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* The emblem + QRAUZ wordmark sit in a subtle accent-tinted pill so the
   top-left corner reads as a deliberate badge rather than floating text.
   Lives here (one shared place) instead of in 23 inline <style> blocks.
   The negative block margin cancels the padding growth so the header height
   (and everything tuned to it, e.g. index's sticky sidebar top) is unchanged. */
header .logo {
  border: 1px solid rgba(114, 137, 218, 0.45);
  background: rgba(114, 137, 218, 0.08);
  border-radius: 16px;
  padding: 0.4rem 1rem 0.4rem 0.55rem;
  margin: -0.45rem 0;
  transition: border-color 0.18s, background 0.18s;
}
header .logo:hover {
  border-color: var(--primary-accent, var(--accent, #7289da));
  background: rgba(114, 137, 218, 0.14);
}

/* ---- Opt-in utilities (apply a class to use; inert otherwise) ----------- */

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Centred content column with fluid side gutters + a max width. */
.q-container { width: 100%; max-width: var(--q-maxw); margin-inline: auto; padding-inline: var(--q-gutter); }
.q-container.q-narrow { max-width: var(--q-maxw-narrow); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Fluid headings / body text. */
.q-h1   { font-size: var(--q-text-2xl); line-height: 1.15; }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-h2   { font-size: var(--q-text-xl);  line-height: 1.20; }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-h3   { font-size: var(--q-text-lg);  line-height: 1.25; }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-text { font-size: var(--q-text-base); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Responsive auto-fit grid: columns collapse to one on small screens with NO
   media query. Tune the wrap point per use with --q-card-min. */
.q-grid {
  display: grid;
  gap: var(--q-space-3);
  grid-template-columns: repeat(auto-fill, minmax(min(var(--q-card-min, 280px), 100%), 1fr));
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Stack on small screens: a flex row that wraps to a column under the given
   width (default 640px). Use .q-stack and set --q-stack-at to override. */
.q-stack { display: flex; flex-wrap: wrap; gap: var(--q-space-2); }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
@media (max-width: 640px) {
  .q-stack.q-stack-tight { flex-direction: column; align-items: stretch; }
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Hide/show by viewport - handy for swapping a desktop control for a mobile
   one without per-page media queries. */
@media (max-width: 768px) { .q-desktop-only { display: none !important; } }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
@media (min-width: 769px) { .q-mobile-only  { display: none !important; } }


/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ═══════════════════════════════════════════════════════════════════════════
   ║ .q-check - THE SITE-STANDARD TICK BOX (v16.82, 2026-08-14)              ║
   ║                                                                         ║
   ║ Rogan: "make the tick boxes on both requests and mylistings thing nice, ║
   ║ site standard looking with softer edges and site colours".              ║
   ║                                                                         ║
   ║ THE BUG: every boolean opt-in on the site was a RAW NATIVE <input       ║
   ║ type=checkbox> with `accent-color` painted on it - post-request.html's  ║
   ║ "Can be done remotely" and mylistings' "Price negotiable". A native     ║
   ║ checkbox is a hard-edged square drawn by the OS, so it looked           ║
   ║ different on every machine, ignored the site's radius language          ║
   ║ entirely, and `accent-color` only tints the fill - the border stays the ║
   ║ platform's. That is the "hard edges / not site colours" complaint.      ║
   ║                                                                         ║
   ║ THIS IS NOW THE ONE COPY. Use it for any new tick box:                  ║
   ║   <label class="q-check" for="x">                                       ║
   ║     <input type="checkbox" id="x">                                      ║
   ║     <span class="q-check-box" aria-hidden="true"><svg .../></span>      ║
   ║     <span class="q-check-label">Label text</span>                       ║
   ║   </label>                                                              ║
   ║ The SVG must be a single <polyline points="20 6 9 17 4 12"/> tick.      ║
   ║                                                                         ║
   ║ WHY IT NEEDS NO LIGHT REPAIR: every colour is a `var(--q-token, <dark   ║
   ║ literal>)` fallback chain, so dark falls through to the literal and     ║
   ║ light picks up the token - the Handbook 18.2 pattern. DO NOT hard-code  ║
   ║ a bare hex in here.                                                     ║
   ║                                                                         ║
   ║ NOT a replacement for .day-checkbox/.day-label (list.html's pill        ║
   ║ chips). Those express "pick several from a set"; this expresses a       ║
   ║ single yes/no with a sentence next to it.                              ║
   ═══════════════════════════════════════════════════════════════════════════ */
/* position:relative CONTAINS the absolutely-positioned input below. Without a
   positioned ancestor it resolves against the viewport, and keyboard focus can
   then scroll the page to wherever that landed. */
.q-check{ position:relative; display:flex; align-items:flex-start; gap:0.65rem; cursor:pointer; -webkit-user-select:none; user-select:none; }
/* The native input stays in the DOM (focus, keyboard, form state, and the
   :checked selector below all depend on it) but is never painted. */
.q-check input[type="checkbox"]{
  position:absolute; opacity:0; width:0; height:0; margin:0; pointer-events:none;
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-check-box{
  flex-shrink:0; width:22px; height:22px; margin-top:0.05rem;
  border-radius:7px;                        /* the "softer edges" */
  border:2px solid var(--q-border-strong, #3a3d42);
  background:var(--q-surface-2, #202225);
  display:inline-flex; align-items:center; justify-content:center;
  color:#fff;
  transition:background 0.13s, border-color 0.13s, box-shadow 0.13s;
}
.q-check-box svg{ width:14px; height:14px; opacity:0; transform:scale(0.6); transition:opacity 0.13s, transform 0.13s; }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-check:hover .q-check-box{ border-color:var(--primary-accent, var(--accent, #7289da)); }
.q-check input[type="checkbox"]:checked + .q-check-box{
  background:var(--primary-accent, var(--accent, #7289da));
  border-color:var(--primary-accent, var(--accent, #7289da));
  box-shadow:0 2px 10px rgba(114,137,218,0.35);
}
.q-check input[type="checkbox"]:checked + .q-check-box svg{ opacity:1; transform:scale(1); }
/* Keyboard users get a visible ring; mouse users do not, because the box
   itself already changes on hover. */
.q-check input[type="checkbox"]:focus-visible + .q-check-box{
  box-shadow:0 0 0 3px rgba(114,137,218,0.45);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-check-label{
  color:var(--secondary-accent, var(--muted, #99aab5));
  font-size:0.95rem; font-weight:500; line-height:1.45;
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-check input[type="checkbox"]:checked ~ .q-check-label{ color:var(--highlight, var(--white, #fff)); }
.q-check input[type="checkbox"]:disabled + .q-check-box{ opacity:0.45; }
.q-check input[type="checkbox"]:disabled ~ .q-check-label{ opacity:0.45; }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ═══════════════════════════════════════════════════════════════════════════
   ║ MOBILE HEADER RULES (v16.92, 2026-08-25)                                ║
   ║                                                                         ║
   ║ WHAT USED TO BE HERE: the .q-hamburger button and the whole .q-drawer   ║
   ║ slide-in panel - about 60 lines of it. Rogan, 2026-08-25: "Hamburger    ║
   ║ menu still exists in the background. Eradicate it." Deleted, not        ║
   ║ hidden: the button markup came out of all 42 pages, setupMobileNav()    ║
   ║ and paintBurgerDot() came out of js/auth-ui.mjs, and the .q-signed-in   ║
   ║ hint that used to hide the button came out of js/shell.mjs and          ║
   ║ scripts/inject-app-shell.mjs. The app shell's five-tab bar is now the   ║
   ║ site's ONLY navigation on a phone, which was the whole point of v16.67. ║
   ║                                                                         ║
   ║ WHAT SURVIVES, AND MUST: everything below is the MOBILE HEADER, not the ║
   ║ hamburger. `header nav{display:none}` and the brand-bar normalisation   ║
   ║ are what make the top bar identical to index.html on every page from    ║
   ║ ~300px up. They predate the hamburger and outlive it. DO NOT DELETE     ║
   ║ THEM as leftovers of a removed feature.                                 ║
   ║                                                                         ║
   ║ ⚠ `header nav{display:none !important}` means a phone has NO header     ║
   ║ links at all. Signed in, the avatar menu covers it; signed out, the     ║
   ║ FOOTER does (it gained Requests / Browse users / Help / About in this   ║
   ║ same change). If you ever remove the tab bar, one of those has to grow  ║
   ║ before this rule can stay.                                              ║
   ║                                                                         ║
   ║ The !important header overrides below are deliberate: pages carry their ║
   ║ own inline header styles (desktop-tuned), and these must win over them  ║
   ║ regardless of load order. Colour values use fallback chains because     ║
   ║ variable names differ across pages (see the header note at the top).    ║
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width:768px){
  /* Hide the page's inline nav. The five tabs replace it (v16.67); until
     v16.92 the hamburger did, and `.q-hamburger{display:inline-flex}` sat on
     this line to show it. Both the button and the drawer are gone now - see
     the banner above - but THIS RULE STAYS: the header <nav> is desktop
     furniture that does not fit a phone, which was true before the hamburger
     existed. */
  header nav{ display:none !important; }
  /* keep the logo + messages/avatar on ONE row. Several pages set
     the header to flex-wrap:wrap, which used to drop the hamburger onto its
     own line; force nowrap and group the controls on the right. Also normalise
     the brand bar to index.html's mobile sizing on EVERY page: most pages
     ship only desktop header rules (logo ~1.9rem, padding 1.4rem 2.5rem),
     so on a phone the oversized logo + padding push the nowrap row past the
     viewport - the bar looks squashed / "zoomed in" and the right-hand
     controls clip when you pinch out. These mirror index.html's
     @media(max-width:900px) header rules; the !important wins over the
     page's own <style> without per-page edits, so the top nav is identical
     to index across the whole site. */
  header{ flex-wrap:nowrap !important; padding:0.75rem 1rem !important; }
  header #authContainer{ margin-left:auto; }
  header .logo{ font-size:1.4rem !important; }
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
@media (max-width:480px){
  header .logo{ font-size:1.3rem !important; }
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* Very narrow phones (down to ~300px): shrink the logo + trim padding so
   the whole bar still fits on one line instead of wrapping. */
@media (max-width:380px){
  header{ padding-left:0.7rem !important; padding-right:0.7rem !important; gap:0.4rem !important; }
  header .logo{ font-size:1.15rem !important; gap:0.35rem !important; }
  header .logo svg, header .logo img{ width:30px !important; height:30px !important; }
  /* v16.92: the .q-hamburger size trim for very narrow phones came out here
     with the button. The logo + avatar are all that share this row now. */
}


/* ═══════════════════════════════════════════════════════════════════════════
   ║ APP SHELL - PERSISTENT TAB BAR / DESKTOP RAIL (v16.67, 2026-08-05)      ║
   ║ DO NOT REVERT WITHOUT READING THIS.                                     ║
   ║                                                                         ║
   ║ BUG (Rogan, 2026-08-05): a competitor's mobile UI "felt ridiculously    ║
   ║ easy to use" next to ours, and we could not say why from the styling.   ║
   ║                                                                         ║
   ║ ROOT CAUSE: not styling. QRAUZ ran FOUR navigation systems at once -    ║
   ║ the header <nav>, the 11-link hamburger drawer, the inbox + avatar      ║
   ║ dropdown, and per-page tab bars - plus index.html's filter pull-tab.    ║
   ║ Nothing on screen stayed put between pages, so there was no fixed       ║
   ║ point of reference anywhere on the site. The competitor has exactly     ║
   ║ ONE: a bottom bar that never moves. That is the entire difference.      ║
   ║                                                                         ║
   ║ THE FIX: five destinations, always visible, same order everywhere.      ║
   ║ ONE DOM (built by js/shell.mjs) serves BOTH breakpoints - a bottom bar  ║
   ║ at <=768px, the identical element rotated to a left rail at >=769px.    ║
   ║ Mobile and desktop are therefore structurally incapable of drifting     ║
   ║ apart, which was the other half of the brief.                          ║
   ║                                                                         ║
   ║ WHY IT IS SAFE:                                                         ║
   ║  - Everything is namespaced .q-tab* / .q-shell*. No bare element        ║
   ║    selectors and NO box-sizing reset: the 33 pages are individually     ║
   ║    inline-styled with no shared reset, and a global rule here would     ║
   ║    shift tuned layouts (see this file's header).                        ║
   ║  - Colours go through the SAME fallback chain as the drawer above       ║
   ║    (var(--header-dark, var(--hd, #1a1c1f))), so the bar themes itself   ║
   ║    in light mode with no separate repair sheet. Two naming schemes      ║
   ║    exist across pages - never assume one.                               ║
   ║  - z-index 90 is deliberate: ABOVE ordinary content, BELOW index.html's ║
   ║    filter backdrop (115) / sidebar (120), page modals (up to 99999),    ║
   ║    dialog.mjs (100000) and the drawer (1400). A persistent bar must     ║
   ║    never float over a modal.                                            ║
   ║  - Dark mode is untouched outside this new surface.                     ║
   ║                                                                         ║
   ║ TRIPWIRES:                                                              ║
   ║  - Hamburger AND tab bar both visible on a phone => body.q-shell-on     ║
   ║    stopped being applied by shell.mjs.                                  ║
   ║  - A page footer disappearing under the bar => the body padding rule    ║
   ║    below stopped matching (it is the ONLY thing reserving that space).  ║
   ║  - Badge floating loose in the middle of a tab => auth-ui.mjs sets      ║
   ║    a.style.position='relative' inline; the .mycal-badge-row override    ║
   ║    below depends on it.                                                 ║
   ═══════════════════════════════════════════════════════════════════════════ */

/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-tabbar{
  display:none;                     /* shown per-breakpoint below */
  position:fixed; left:0; right:0; bottom:0; z-index:90;
  background:var(--header-dark,var(--hd,#1a1c1f));
  box-shadow:0 -2px 12px rgba(0,0,0,0.35);
  /* env() resolves to 0 without viewport-fit=cover, which we deliberately do
     NOT set - adding it would change rendering on every notched device across
     all 33 pages. This is future-proofing only, not a live offset. */
  padding-bottom:env(safe-area-inset-bottom,0px);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-tab{
  flex:1 1 0; min-width:0;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:0.15rem; padding:0.5rem 0.15rem 0.45rem;
  text-decoration:none; -webkit-tap-highlight-color:transparent;
  color:var(--secondary-accent,var(--muted,#99aab5));
  transition:color 0.15s;
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-tab-ico{ position:relative; display:flex; align-items:center; justify-content:center; }
.q-tab-ico svg{ width:23px; height:23px; display:block; }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-tab-label{
  font-size:0.68rem; font-weight:600; letter-spacing:0.01em;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%;
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-tab[aria-current="page"]{ color:var(--primary-accent,var(--accent,#7289da)); }
.q-tab:active{ opacity:0.65; }
/* background IS EXPLICIT AND LOAD-BEARING - DO NOT DELETE AS "redundant".
   Belt-and-braces against the bug the <div role="navigation"> already fixes
   (see shell.mjs): index.html:86 has an UNSCOPED `nav a:hover{background:
   var(--accent)}`. If a page ever does the same to a bare `a:hover`, this line
   is what stops an accent slab painting over an accent-coloured label and
   making the whole tab invisible. Declaring it means the shell owns its own
   background in every state instead of inheriting whatever a page assumes. */
.q-tab, .q-tab:hover, .q-tab:focus{ background:transparent; }
/* Hover is an ACCENT WASH, not accent text on whatever background happens to
   be underneath - the wash reads at a glance and cannot collide with the label
   colour. Accent-tinted rgba works in both themes without a light repair. */
.q-tab:hover{ color:var(--primary-accent,var(--accent,#7289da)); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* The raised centre action. Listing supply is the growth constraint, so List
   gets the most-tapped position on the screen instead of being buried two taps
   deep on a profile screen the way the competitor does it. */
.q-tab-raised .q-tab-ico{
  width:46px; height:46px; margin-top:-16px; border-radius:50%;
  background:var(--primary-accent,var(--accent,#7289da));
  color:#fff;
  box-shadow:0 4px 14px rgba(0,0,0,0.35);
}
.q-tab-raised .q-tab-ico svg{ width:25px; height:25px; stroke-width:2.4; }
.q-tab-raised .q-tab-label{ margin-top:-2px; }
.q-tab-raised[aria-current="page"] .q-tab-ico{ filter:brightness(1.12); }

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ── Badge paint targets ───────────────────────────────────────────────────
   NO counting happens here or in shell.mjs. The red/blue rule already lives in
   auth-ui.mjs setupMyCalBadges + mycalendars.html mycalTabKeys and those two
   are under a standing instruction to agree - a third copy would be the first
   to drift. These rules only RE-POSITION the badges auth-ui.mjs already paints.
   The ring uses the bar's own background var, so it matches in both themes
   instead of keeping the hard-coded #1a1c1f ring the header badges carry. */
.q-tab-badge-row{
  position:absolute; top:-5px; left:calc(50% + 2px);
  display:flex; align-items:center; gap:2px; pointer-events:none;
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
.q-tab-badge{
  color:#fff; font-size:0.62rem; font-weight:700;
  min-width:15px; height:15px; border-radius:8px;
  display:none;                     /* shown by paintInboxBadges when > 0 */
  align-items:center; justify-content:center; padding:0 4px;
  box-shadow:0 0 0 2px var(--header-dark,var(--hd,#1a1c1f));
}
.q-tab-badge.chat { background:#7289da; }
.q-tab-badge.notif{ background:#f04747; }
/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* setupMyCalBadges appends .mycal-badge-row to the <a> itself (top:-7px;
   right:-12px), which would land outside the tab. Re-anchor it over the icon.
   It relies on the inline position:relative auth-ui.mjs sets on the anchor. */
.q-tabbar .mycal-badge-row{ top:3px; right:auto; left:calc(50% + 2px); }
.q-tabbar .mycal-badge{ box-shadow:0 0 0 2px var(--header-dark,var(--hd,#1a1c1f)); }

/* ══════════════════════════════════════════════════════════════════════════
   ║ v16.78 (2026-08-11) WHY EVERY SHELL RULE BELOW IS WRITTEN TWICE        ║
   ║ DO NOT "DE-DUPLICATE" THESE SELECTOR LISTS.                            ║
   ║                                                                        ║
   ║ BUG (Rogan, 2026-08-11, cold load of qrauz.com): the site painted the  ║
   ║ OLD pre-v16.67 layout for a second or two on every navigation - header ║
   ║ nav visible, no rail, no reserved gutter - then swapped to the shell.  ║
   ║                                                                        ║
   ║ ROOT CAUSE: every rule here keyed off `body.q-shell-*`, and those      ║
   ║ classes were added by js/shell.mjs, which cannot run until ~357KB of   ║
   ║ modules (supabase-js first) has downloaded and evaluated. The CSS was  ║
   ║ correct and present in <head> the whole time; it simply had nothing to ║
   ║ match yet.                                                             ║
   ║                                                                        ║
   ║ FIX: scripts/inject-app-shell.mjs stamps the same classes onto the     ║
   ║ <html> element in the page source, so `html.q-shell-*` matches in the  ║
   ║ FIRST paint, before a byte of JavaScript runs. Both forms are kept:    ║
   ║   html.q-shell-*  = the static, author-time, zero-JS path (normal)     ║
   ║   body.q-shell-*  = shell.mjs's runtime path, still the fallback for a ║
   ║                     page the generator has not stamped or a stale      ║
   ║                     cached document.                                   ║
   ║ Declarations are IDENTICAL in each pair, so the settled appearance is  ║
   ║ unchanged in both themes - only the moment it appears has moved.       ║
   ║                                                                        ║
   ║ TRIPWIRE: delete the html.* halves and the old-layout flash comes      ║
   ║ straight back. Delete the body.* halves and any unstamped page loses   ║
   ║ its shell entirely.                                                    ║
   ══════════════════════════════════════════════════════════════════════════ */

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ── Mobile: bottom bar ──────────────────────────────────────────────────── */
@media (max-width:768px){
  html.q-shell-on .q-tabbar,
  body.q-shell-on .q-tabbar{ display:flex; align-items:stretch; }
  /* The ONLY thing reserving space for the fixed bar. Without it every page's
     footer sits underneath it.
     box-sizing IS LOAD-BEARING HERE - DO NOT DELETE IT AS "tidying".
     19 pages set `body{min-height:100vh}` and 19 ship no box-sizing reset, so
     on a content-box body this padding would ADD to 100vh and give every short
     page ~58px of dead scroll. Scoped to this one element and only when the
     shell is on, which is nothing like the global `*` reset this file
     deliberately avoids (see the file header): body has no explicit width or
     height anywhere in the project - checked - so border-box changes only how
     min-height and this padding compose, which is exactly the intent. */
  html.q-shell-on body,
  body.q-shell-on{
    box-sizing:border-box;
    padding-bottom:calc(58px + env(safe-area-inset-bottom,0px));
  }
  /* ── THE HAMBURGER-HIDING RULE IS GONE (v16.92, 2026-08-25) ─────────────
     Three selectors hid `.q-hamburger` here whenever we believed the visitor
     was signed in. Rogan: "Hamburger menu still exists in the background.
     Eradicate it." - so the button, the drawer, the overlay and the whole
     .q-signed-in hint that drove this rule were deleted rather than hidden.
     WHY THAT IS SAFE, since this comment used to say the opposite: the old
     note warned that dropping .q-signed-in would strand Help / About /
     Contact / Browse users on phones for logged-out visitors, because
     settings.html needs a login. It would have - so in the SAME change the
     site footer gained Requests / Browse users / Help / About, which is now
     their route on a phone. Do not delete those footer links.
     TRIPWIRE: a menu button reappearing in a phone header means the static
     markup came back. Grep the page source for q-hamburger. */
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ── Desktop: the same element as a left rail ────────────────────────────────
   Behind its own flag (SHELL_RAIL in shell.mjs) because this is the higher-risk
   half - it changes desktop layout on all 33 pages at once. With the flag off,
   body never gets .q-shell-rail, the bar simply does not render above 768px,
   and desktop is byte-identical to before. */
@media (min-width:769px){
  /* WIDTH: 64px, narrowed from 76px (Rogan, 2026-08-05 - "rail feels too wide
     on desktop"). The floor is the longest label, "Calendar" (renamed from
     "Activity" in v16.83, same 8 characters, so the measurement below still
     holds): at 0.66rem Inter semibold it measures ~46px, leaving ~9px of gutter
     each side. Going below ~60px starts truncating it, at which point drop the
     labels and go icon-only with tooltips rather than shipping "Calend...". */
  html.q-shell-rail .q-tabbar,
  body.q-shell-rail .q-tabbar{
    display:flex; flex-direction:column; align-items:stretch; justify-content:flex-start;
    top:0; right:auto; width:64px; bottom:0;
    padding:0.9rem 0 1rem; gap:0.15rem;
    box-shadow:2px 0 12px rgba(0,0,0,0.35);
  }
  /* Reserves the rail's column. header/footer are in normal flow (the header is
     position:sticky, which respects the padding box), so both inset with it and
     neither can end up underneath the rail. MUST equal the width above. */
  html.q-shell-rail body,
  body.q-shell-rail{ padding-left:64px; }
  html.q-shell-rail .q-tab,
  body.q-shell-rail .q-tab{ flex:0 0 auto; padding:0.65rem 0.15rem; gap:0.25rem; }
  html.q-shell-rail .q-tab-label,
  body.q-shell-rail .q-tab-label{ font-size:0.66rem; }
  /* The rail has room for a hit-target wash; the phone bar does not. Accent at
     12% sits under both the light and dark label colours with contrast to
     spare, so no separate light repair is needed. */
  html.q-shell-rail .q-tab:hover,
  body.q-shell-rail .q-tab:hover{
    color:var(--primary-accent,var(--accent,#7289da));
    background:rgba(114,137,218,0.12);
    border-radius:10px;
  }
  html.q-shell-rail .q-tab,
  body.q-shell-rail .q-tab{ margin:0 0.35rem; }
  /* The rail now owns these five destinations, so the header's copy of them is
     duplicate navigation - which is the exact problem this change exists to
     fix. Logo and avatar/inbox stay. */
  /* THE FLASH RULE. This is the one that was arriving 357KB late - the header's
     five links are the "old display" Rogan photographed. html.q-shell-rail is
     in the page source, so the duplicate nav is never painted even once. */
  html.q-shell-rail header nav,
  body.q-shell-rail header nav{ display:none !important; }
  html.q-shell-rail .q-tab-raised .q-tab-ico,
  body.q-shell-rail .q-tab-raised .q-tab-ico{ margin-top:0; width:44px; height:44px; }
}

/* NEVER CHANGE UNLESS MADE TO CHANGE */
/* ── Light-mode repair ──────────────────────────────────────────────────────
   Scoped to html[data-theme="light"], which is structurally incapable of
   altering dark (see the LIGHT-MODE REPAIR SHEET header above). The bar's
   background/text/accent all resolve correctly through the var chain already;
   only the shadow needs work, because a heavy black drop shadow reads as a
   smudge on white. Mirrors what the header repair does. */
html[data-theme="light"] .q-tabbar{
  box-shadow:0 -1px 0 var(--q-border), 0 -2px 14px rgba(20,22,28,0.06);
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
@media (min-width:769px){
  html[data-theme="light"].q-shell-rail .q-tabbar,
  html[data-theme="light"] body.q-shell-rail .q-tabbar{
    box-shadow:1px 0 0 var(--q-border), 2px 0 14px rgba(20,22,28,0.06);
  }
}
/* NEVER CHANGE UNLESS MADE TO CHANGE */
html[data-theme="light"] .q-tab-raised .q-tab-ico{ box-shadow:0 4px 14px rgba(91,104,204,0.35); }