  :root, :root[data-theme="light"] {
    color-scheme: light;
    /* ── Font ── */
    --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

    /* ── Surfaces (light) — matched to the reviewed wireframes' own palette ── */
    --bg: #F3F4F6;
    --surface: #FFFFFF;
    --surface2: #F6F7F9;
    --surface3: #EEF0F3;      /* hover state on top of surface2 */
    --border: rgba(0,0,0,0.09);
    --border2: rgba(0,0,0,0.14);
    --overlay: rgba(6,8,12,.68);

    /* Chrome — independently colourable UI shell pieces (theme editor's
       "granular" controls). */
    --header-bg: #FFFFFF;
    --sidebar-bg: #f0f1f3;
    --menu-bg: #FFFFFF;

    /* ── Text — four greys, strictly ── */
    --text: #16181D;          /* primary — names, prices, values */
    --text-secondary: #585E68; /* subtitles, body */
    --muted: #8A909A;         /* dates, sources, labels, counts */
    --dim: #A6ABB3;           /* "None", empty, disabled */

    /* Brand red — UI chrome, buttons, active states. -hover is for hover/
       active states on filled buttons; -contrast is the text colour that
       sits on top of a filled brand button. */
    --brand: #e8251a;
    --brand-hover: #cf3028;
    --brand-contrast: #ffffff;
    --brand-bg: rgba(232,37,26,0.08);
    --brand-border: rgba(232,37,26,0.28);
    /* Semantic colours, each with three roles:
         INK  (--green)      text/icons on a plain surface — must hold AA contrast
         FILL (--green-fill) a filled button/bar background — can be brighter
         the FILL's own text colour is --green-contrast, etc. */
    /* Neutral fill for the Archive swipe reveal (Luke, 2026-09-22: "GREY is the reveal colour" —
       separate from --muted/--dim, which are text-weight greys too pale for a solid, white-text
       swipe background). */
    --grey-fill: #6b7280;
    --green: #12A05B;
    --green-fill: #45c77c;
    --green-contrast: #061c0f;
    --green-bg: rgba(18,160,91,0.1);
    --green-border: rgba(18,160,91,0.25);
    --amber: #D97706;
    --amber-fill: #f3a24f;
    --amber-contrast: #251300;
    --amber-bg: rgba(217,119,6,0.1);
    --amber-border: rgba(217,119,6,0.25);
    /* Best-deal gold — distinct from --amber (warning/negotiate), used for
       the Wins page "your best deal" hero accent and the "Mark as Won"
       alert CTA (same gold, so claiming a win visually previews the
       Wins-page treatment it's about to get). */
    --best: #DE8E1C;
    --best-border: rgba(222,142,28,0.3);
    --best-bg: rgba(222,142,28,0.15);
    --red: #E24B4A;
    --red-fill: #e1625b;
    --red-bg: rgba(226,75,74,0.08);
    --blue: #1a6fd6;
    --blue-bg: rgba(26,111,214,0.1);
    --blue-border: rgba(26,111,214,0.25);
    /* Boost — watch check-rate speed-ups (coming soon), kept distinct from brand/semantic colours */
    --boost: #6a5cd6;
    --boost-bg: rgba(106,92,214,0.1);
    --boost-border: rgba(106,92,214,0.32);
    --boost-deep: #5140b0;

    /* ── Radii — nested elements step down from their parent ── */
    --r-card: 16px;      /* card */
    --r-nested: 14px;    /* nested card */
    --r-tile: 12px;      /* stat tile */
    --r-control: 10px;   /* segmented control, menu */
    --r-field: 9px;      /* field, button */
    --r-badge: 8px;       /* small thumbnail, badge */
    --r-item: 7px;        /* menu item, control option */
    --r-pill: 20px;       /* chip / pill */
    --radius: var(--r-field);
    --radius-lg: var(--r-card);

    /* ── Spacing ── */
    --sp-page: 16px;
    --sp-card: 14px;
    --sp-compact: 12px;
    --sp-stack: 10px;
    --sp-grid: 8px;

    /* ── Shadows — light theme keeps these subtle; dark theme below goes deeper ── */
    --shadow-sm: 0 1px 2px rgba(16,24,40,.05);
    --shadow-md: 0 8px 24px rgba(18,22,29,.12);
    --shadow-lg: 0 16px 40px rgba(18,22,29,.16);

    /* ── Tutorial card — deliberately INVERTED from the rest of the
       theme (Luke: make it light in dark mode and dark in light mode,
       "to make it stand out"), so these don't just mirror --surface/
       --text like everything else. Site is light here -> card is dark. ── */
    --tutorial-bg: #1E1C17;
    --tutorial-surface2: #2A271F;
    --tutorial-text: #F5F3EE;
    --tutorial-text-secondary: #C9C4B6;
    --tutorial-muted: #948E7E;
    --tutorial-border: #3C382E;
    /* Tom, 2026-09-18: "make the tutorial buttons and callouts a nice
       dark purple, so it's like nothing else on the site" — a dedicated
       accent, not var(--brand)'s red, kept identical in both themes
       (an identity color, not something that should invert). */
    --tutorial-accent: #7C5CFA;
    --tutorial-accent-rgb: 124, 92, 250;

    /* ── Shop info card (Credits/Watch Tokens), neutral variant — Luke,
       2026-09-18: the Credits explainer used the brand-red tint (see
       .shop-info-card.credits below) and "looked like a red error
       message". Deliberately dark regardless of site theme, same idea
       as the tutorial card above, but not a full inversion — dark mode's
       own page background is already near-black, so this needs its own
       distinct-from-page-bg shade there too, not just "dark" in the
       abstract. ── */
    --shop-info-neutral-bg: #1c1e24;
    --shop-info-neutral-border: rgba(255,255,255,0.12);
    --shop-info-neutral-text: #f5f3ee;
    --shop-info-neutral-text-secondary: #c9ccd1;
  }

  /* ── Dark theme — same variable names, same components; nothing outside
     this block and the toggle button/script needs to know which is active. ── */
  :root[data-theme="dark"] {
    color-scheme: dark;
    --bg: #0f1013;
    --surface: #181a1f;
    --surface2: #202329;
    --surface3: #292d34;
    --header-bg: #15171b;
    --sidebar-bg: #121418;
    --menu-bg: #202329;
    --border: rgba(255,255,255,.07);
    --border2: rgba(255,255,255,.14);
    --overlay: rgba(0,0,0,.76);

    --text: #f2f3f5;
    --text-secondary: #bdc1c8;
    --muted: #8f959f;
    --dim: #666d78;

    --brand: #ff6259;
    --brand-hover: #ff7a72;
    --brand-contrast: #190302;
    --brand-bg: rgba(255,98,89,.12);
    --brand-border: rgba(255,98,89,.34);

    --grey-fill: #7d8694;
    --green: #65dc96;
    --green-fill: #45c77c;
    --green-contrast: #04170b;
    --green-bg: rgba(101,220,150,.12);
    --green-border: rgba(101,220,150,.30);
    --amber: #f6b562;
    --amber-fill: #efa04c;
    --amber-contrast: #211000;
    --amber-bg: rgba(246,181,98,.12);
    --amber-border: rgba(246,181,98,.30);
    --best: #f0a83a;
    --best-border: rgba(240,168,58,.34);
    --best-bg: rgba(240,168,58,.2);
    --red: #ff746c;
    --red-fill: #e1625b;
    --red-bg: rgba(255,116,108,.12);
    --blue: #6babf4;
    --blue-bg: rgba(107,171,244,.12);
    --blue-border: rgba(107,171,244,.30);
    --boost: #a397ff;
    --boost-deep: #8071ee;
    --boost-bg: rgba(163,151,255,.13);
    --boost-border: rgba(163,151,255,.34);

    --shadow-sm: 0 1px 2px rgba(0,0,0,.22);
    --shadow-md: 0 10px 28px rgba(0,0,0,.38);
    --shadow-lg: 0 18px 52px rgba(0,0,0,.50);

    /* Inverted the other way — site is dark here, so the tutorial card
       goes light. */
    --tutorial-bg: #FFFFFF;
    --tutorial-surface2: #F1F0EC;
    --tutorial-text: #16181D;
    --tutorial-text-secondary: #4B505A;
    --tutorial-muted: #7A7F89;
    --tutorial-border: #E1DFDA;
    --tutorial-accent: #7C5CFA;
    --tutorial-accent-rgb: 124, 92, 250;

    /* Shop info card neutral variant — NOT inverted like the tutorial
       card above (that would make it light here, which is the opposite
       of what "dark grey that stands out" means). Page bg is already
       near-black (#0f1013) in this theme, so this needs to be a shade
       lighter than that to actually read as a distinct card rather than
       black-on-black — still unambiguously "dark grey", just calibrated
       against this theme's own darkness. */
    --shop-info-neutral-bg: #26292f;
    --shop-info-neutral-border: rgba(255,255,255,0.12);
    --shop-info-neutral-text: #f2f3f5;
    --shop-info-neutral-text-secondary: #bdc1c8;
  }

  body, header, aside, .pane, .metric, .alert-item, .watch-card, .wcm, .wc2-card, .wd-card {
    transition: background-color .2s ease, color .2s ease, border-color .2s ease;
  }
  @media (prefers-reduced-motion: reduce) {
    body, header, aside, .pane, .metric, .alert-item, .watch-card, .wcm, .wc2-card, .wd-card { transition: none; }
  }

  :root[data-theme="dark"] img.logo-desktop,
  :root[data-theme="dark"] img.logo-mobile { filter: brightness(.98) saturate(.94); }

  /* Theme + sound toggles live inside the account menu (desktop dropdown
     and mobile burger menu both use the same .user-dd-item markup/classes)
     rather than as standalone header buttons. */
  .dd-theme-toggle .moon { display: none; }
  :root[data-theme="dark"] .dd-theme-toggle .sun { display: none; }
  :root[data-theme="dark"] .dd-theme-toggle .moon { display: block; }
  .dd-sound-toggle .icon-off { display: none; }
  .dd-sound-toggle.muted .icon-on { display: none; }
  .dd-sound-toggle.muted .icon-off { display: block; }

  * { box-sizing: border-box; margin: 0; padding: 0; }

  /* ── Sora font experiment (2026-08-09) ──────────────────────────────
     Scoped to the Dashboard page and the shared alert/watch card
     components only, not the whole app — see if it reads as more
     impactful/clean like the home page before rolling it out further,
     or rolling it back. --font is a custom property, and those inherit,
     so redefining it here is enough to flip every existing
     `font-family: var(--font)` declaration within these containers to
     Sora with zero changes to those individual rules — reverting this
     experiment is just deleting this rule block. */
  #pane-dashboard, #pane-pricing, #pane-referral, #pane-changelog, #pane-settings, #pane-wallet, #pane-shop, #pane-archive, #pane-add, #pane-detail, #affiliate-page,
  .alert-item, .wc2-card, .wcm, .wc2-add,
  .sb-stats, .header-nav, .user-dropdown, .mob-menu, .wallet-strip, .alert-bulk-bar,
  #alerts-page-heading, #watches-page-heading, #archive-page-heading, .alert-filter-chip, .empty-icon, .empty-text, .cc-toast,
  /* Shop's basket modal (and the competition Reveal modal, the
     first-watch-reward modal, and the beta welcome popup, same reason)
     is appended outside its owning pane's own DOM subtree (same
     top-level-overlay pattern every modal in this file uses), so it
     fell outside this scope by accident — it reads as an inconsistent
     mismatch against the rest of the page without Sora too. */
  #shop-basket-overlay, #comp-reveal-overlay, #first-watch-reward-overlay, #beta-welcome-overlay,
  /* every modal overlay, so no modal is left in the system font (Luke, 2026-09-21: modal fonts didn't match) */
  .overlay,
  /* Site footer sits outside every pane, so it was still the system font (Luke, 2026-09-20). */
  .site-footer {
    --font: 'Sora', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    /* Redefining --font alone isn't enough: elements with no font-family
       of their own (e.g. .page-heading) inherit body's already-computed
       font-family, resolved before this scope existed — not a live re-
       read of the variable. Setting font-family here too gives every
       descendant a fresh computed value to inherit from. !important
       because a couple of targeted selectors (.alert-filter-chip) set
       their own conflicting font-family later in the stylesheet at the
       same specificity, which would otherwise win on source order. */
    font-family: var(--font) !important;
  }

  /* Semantic type categories — first pass, card-family text only for now.
     Weights mirror the home page's own convention (home.css): 700 for
     anything with visual importance (titles, prices), 600 for medium
     emphasis (section labels), 400 for everything else. Applied as an
     additional class alongside each element's existing component class,
     so component-specific styling (colour, layout) is untouched and only
     typography is taken over here. !important is deliberate, not lazy:
     several of the selectors being overridden (e.g. `.wc2-price .v`,
     `.dash-tile-val.green`) are compound and outrank a single new class
     on specificity alone, tie or no tie on source order — without it,
     this class would silently lose on exactly the elements it's meant to
     retarget. This is the one place in the file that uses !important. */
  /* line-height/letter-spacing on the three "big figure" categories mirror
     home.css's own headline treatment (tight line-height + slightly
     negative tracking on anything large) — page_section_title already
     matches home's small-uppercase-label convention as-is (.section-head
     already carries letter-spacing:.06em/uppercase), so it's untouched
     here; card_title/card_sub_title/card_table_prices are compact
     data-row text, not headline moments, so left at normal spacing too. */
  /* page_title matches home's .hero-headline treatment exactly (uppercase,
     700, line-height 1.08, letter-spacing -.01em) — confirmed against
     home.css rather than guessed, since all-caps text usually wants
     positive tracking, but home's own big uppercase headline uses
     negative, so that's what we mirror here. */
  .page_title { font-size: 32px !important; font-weight: 700 !important; line-height: 1.08 !important; letter-spacing: -.01em !important; text-transform: uppercase !important; }
  /* +10px top-padding is section titles only (.page_section_title) — read
     "which" in the request as referring to the immediately preceding
     "section titles", not dash/card titles too. Flag if that's wrong. */
  .page_section_title { font-size: 14px !important; font-weight: 700 !important; padding-top: 10px !important; }
  .dash_tile_title { font-size: 14px !important; font-weight: 700 !important; }
  .card_title { font-size: 14px !important; font-weight: 700 !important; }
  .card_sub_title { font-size: 12px !important; font-weight: 400 !important; }
  .card_prices { font-size: 20px !important; font-weight: 700 !important; line-height: 1.1 !important; letter-spacing: -.01em !important; }
  .card_stat_prices { font-size: 26px !important; font-weight: 700 !important; line-height: 1.1 !important; letter-spacing: -.01em !important; }
  .card_table_prices { font-size: 13px !important; font-weight: 700 !important; }
  /* Floor tier: labels, timestamps, hints, small badges — nothing in the
     app should render smaller than this. Replaces the 9/9.5/10/10.5px
     values legacy CSS used for this same role. */
  .meta_text { font-size: 11px !important; font-weight: 400 !important; }
  /* Between card_sub_title (12px) and card_prices (20px) — a value that
     needs to read as more prominent than body text but isn't a headline
     price/stat (e.g. a threshold £ figure, a compact stat-tile number). */
  .value_emphasis { font-size: 16px !important; font-weight: 700 !important; }

  body {
    background: var(--bg);
    color: var(--text);
    font-family: var(--font);
    -webkit-font-smoothing: antialiased;
    font-size: 13px;
    min-height: 100vh;
    min-width: 360px;
    /* Mobile Chrome/Safari can silently inflate small text (their own
       "readability" font-boosting heuristic) past whatever a rule
       actually declares — form controls like <select> are a common
       target for it. Luke, 2026-09-14: "our question drop downs are
       the same size large text... 16px where everything else is...
       13px and 12px" — this is the standard fix (effectively every CSS
       reset includes it): disables that automatic inflation site-wide
       so every declared font-size, everywhere, renders as declared. */
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
    /* Backstop for the iPad Safari horizontal-scrollbar bug fixed
       alongside this (see aside's own overflow-x comment) — main
       already had overflow-x:hidden, body didn't, and aside is
       position:fixed so it sits outside main's box entirely. Any
       future stray overflow from a fixed/absolute element now gets
       clipped here instead of becoming a whole-page horizontal
       scrollbar again. */
    overflow-x: hidden;
  }

  /* ── Layout ──
     Header spans the full browser width edge-to-edge. Sidebar is pinned to
     the true left edge (position:fixed), full height, independent of the
     centered content — PokePulse-style. Only .pane (the main content area)
     is centered with a max-width, on its own terms within the remaining
     space to the right of the sidebar. */
  .app { min-height: 100vh; }

  /* ── Dashboard-pane banners (shared shape) ──────────────────────────────
     One visual family for every "you need to do something" banner that
     lives as a normal-flow card inside #pane-dashboard, above the
     "Dashboard" title — Connect Discord (blurple), Verify email (red),
     and Update available (amber) all stack here now (moved 2026-09-11;
     email used to be a full-page-width strip above .app, updates a
     position:fixed strip over everything). Each color is a fixed brand
     hex, not a theme variable — same reasoning as Discord's own blurple
     below: these are deliberate, recognizable colors that shouldn't
     shift with light/dark mode (a theme-variable amber/red goes pastel
     in dark mode, which reads far weaker against white banner text). */
  .pane-banner {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 10px 16px;
    box-sizing: border-box;
    padding: 7px 20px 14px 20px;
    margin-bottom: 20px;
    border-radius: var(--radius-lg);
    color: #fff;
    font-size: 13px;
    font-weight: 600;
  }
  .pane-banner .pane-banner-icon { width: 20px; height: 20px; flex-shrink: 0; font-size: 18px; line-height: 1; }
  .pane-banner span.pane-banner-text { max-width: 560px; }
  .pane-banner .pane-banner-actions { display: flex; align-items: center; flex-shrink: 0; gap: 8px; position: relative; top: 3px; }
  /* Icon + text as one inline unit inside a single span (Luke,
     2026-09-11) — icon and text used to be separate elements, which
     could land the icon alone on its own line above the text on wrap.
     Now the icon renders inline as the first "character" of the text
     itself, so it always wraps together with it. */
  .pane-banner-msg span { display: inline; }
  @media (max-width: 600px) {
    .pane-banner-msg { text-align: center; }
  }
  /* Close X — pinned to the card's own top-right corner (Luke, 2026-09-11)
     rather than sitting inline in .pane-banner-actions, so it always
     reads as "dismiss this card" and never competes for space with the
     actual action button next to it. */
  .pane-banner-close {
    position: absolute; top: 8px; right: 8px;
    background: none; border: none; color: inherit; opacity: .7;
    font-size: 11px; cursor: pointer; line-height: 1; padding: 6px;
    border-radius: var(--radius);
  }
  /* Nudged into place as an inline icon leading the text (baseline
     alignment alone sits it too high/right relative to the first line). */
  #discord-connect-banner svg { width: 20px; height: 20px; flex-shrink: 0; position: relative; top: 5px; left: -7px; }
  #profile-complete-banner svg { width: 20px; height: 20px; flex-shrink: 0; position: relative; top: 5px; left: -7px; }
  /* #payment-failed-banner was missing here, so its warning triangle rendered at the SVG default size (huge) — fixed 2026-09-21. */
  #update-banner svg, #verify-email-banner svg, #payment-failed-banner svg { width: 18px; height: 18px; flex-shrink: 0; position: relative; top: 4px; left: -4px; }
  .pane-banner-close:hover { opacity: 1; background: rgba(255,255,255,.14); }
  .pane-banner .pane-banner-btn {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    background: #fff;
    padding: 6px 16px;
    border: none;
    border-radius: 999px;
    font-size: 13px;
    font-weight: 700;
    font-family: inherit;
    text-decoration: none;
    cursor: pointer;
    transition: background .15s;
  }
  .pane-banner .pane-banner-dismiss {
    background: none;
    border: 1px solid rgba(255,255,255,.4);
    color: #fff;
    padding: 6px 14px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
  }
  .pane-banner .pane-banner-dismiss:hover { background: rgba(255,255,255,.12); }

  .pane-banner-email { background: #E8251A; }
  .pane-banner-email .pane-banner-btn { color: #E8251A; }
  .pane-banner-email .pane-banner-btn:hover { background: #FBDAD7; }

  .pane-banner-updates { background: #D97706; }
  .pane-banner-updates .pane-banner-btn { color: #D97706; }
  .pane-banner-updates .pane-banner-btn:hover { background: #FBEACC; }

  /* Payment-failed grace-period banner (Subscriptions Relaunch — Tom+
     Luke, 2026-09-16: "display a payment failed banner at the top of
     the dashboard pages") — same amber "act soon" treatment as Update
     Available above, own class so its own copy/CTA can change
     independently. */
  .pane-banner-payment { background: #D97706; }
  .pane-banner-payment .pane-banner-btn { color: #D97706; }
  .pane-banner-payment .pane-banner-btn:hover { background: #FBEACC; }

  /* Referral rewards pending-verification banner (Tom, 2026-09-17
     referral feedback #1) — deliberately green, distinct from the
     amber "act soon" and red "urgent" families above since this is
     good news (money's already earned, just locked) not a warning. */
  .pane-banner-referral { background: #16A34A; }
  .pane-banner-referral .pane-banner-btn { color: #16A34A; }
  .pane-banner-referral .pane-banner-btn:hover { background: #D6F5E3; }

  /* "Since your last visit" alert summary — deliberately not a solid
     fill like Discord/email/updates above: this one reuses the existing
     translucent alert-card treatment (brand-bg/brand-border, the same
     bell icon as the Alerts nav tab, act-btn primary button) rather
     than inventing a fourth solid color (Luke, 2026-09-11: "Option B,
     ... use the existing styling cues"). */
  .pane-banner-alerts {
    background: var(--brand-bg);
    border: 1px solid var(--brand-border);
    color: var(--text);
    font-weight: 400;
    font-size: 13px;
    justify-content: space-between;
  }
  .pane-banner-alerts b { font-weight: 700; }
  .pane-banner-alerts .pane-banner-icon { color: var(--brand); }
  .pane-banner-alerts .since-line { display: inline; }
  /* Bell rendered inline as the first glyph of the text now (same
     .pane-banner-msg pattern as Discord/Updates) — needs its own gap
     and a small downward nudge since it no longer gets the old flex
     row's gap for spacing. */
  .pane-banner-alerts .pane-banner-msg svg { margin-right: 6px; position: relative; top: 4px; }
  @media (max-width: 800px) {
    .pane-banner-alerts { flex-direction: column; align-items: stretch; }
    .pane-banner-alerts .pane-banner-actions .act-btn {
      width: 100%;
      margin-bottom: 4px;
      font-size: 13px;
      font-weight: 700;
      padding: 9px 12px;
    }
    .pane-banner-alerts .since-line { display: block; margin-top: 2px; }
  }

  /* ── "Connect Discord" banner ── */
  /* Discord's own brand blurple (#5865F2) + documented hover shade
     (#4752C4) — same one deliberate "real brand color" exception used on
     /beta's own Discord button, since recognizability is the whole point
     here. Lives as a normal-flow card inside #pane-dashboard itself
     (matching .dash-tile's own border-radius/padding scale), not a
     full-page-width strip — sits above the "Dashboard" title, on the
     Dashboard tab only. Originally no close/dismiss (only replaced once
     discord_connected is actually true, per loadCurrentUser in
     dashboard.js) — Luke asked for both a close X and a Dismiss button
     anyway (2026-09-11); it can still reappear on a later visit/reload
     while the account remains unconnected, same as Updates. Shared
     shape now lives in .pane-banner above (email/updates banners moved
     alongside it, 2026-09-11) — this ID just carries the one thing
     that's actually Discord-specific: the brand color and its
     svg/anchor children, which predate .pane-banner's own generic
     .pane-banner-icon/.pane-banner-btn class names. */
  #discord-connect-banner { background: #5865F2; }
  #discord-connect-banner span { max-width: 560px; }
  #discord-connect-banner a {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    background: #fff;
    color: #5865F2;
    padding: 6px 16px;
    border-radius: 999px;
    font-size: 13px;
    font-weight: 700;
    text-decoration: none;
    transition: background .15s;
  }
  #discord-connect-banner a:hover { background: #E8E9FD; }

  /* "Complete your profile" banner (Luke, 2026-09-24) — same white-pill-
     on-solid-bar shape as Discord's above, deliberately a different color
     (violet, not Discord's blurple) so the two never read as one family. */
  #profile-complete-banner { background: #7C3AED; }
  #profile-complete-banner span { max-width: 560px; }
  #profile-complete-banner a {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    background: #fff;
    color: #7C3AED;
    padding: 6px 16px;
    border-radius: 999px;
    font-size: 13px;
    font-weight: 700;
    text-decoration: none;
    transition: background .15s;
  }
  #profile-complete-banner a:hover { background: #EDE4FD; }

  /* Open Beta welcome popup's own Connect Discord button — real brand
     blurple/hover, same pair as #discord-connect-banner above, just a
     solid full-width button here instead of a white pill on a purple
     bar. */
  #beta-welcome-discord-btn {
    padding: 10px 22px; background: #5865F2; color: #fff;
    border: none; border-radius: var(--radius); font-family: var(--font);
    font-size: 14px; font-weight: 600; cursor: pointer; transition: background .15s;
  }
  #beta-welcome-discord-btn:hover { background: #4752C4; }

  /* Email-verification banner — was a full-page-width strip above .app
     (hardcoded white-on-translucent-red, fixed by Tom 2026-09-04 for a
     light-mode contrast issue). Moved 2026-09-11 into #pane-dashboard as
     a normal-flow .pane-banner-email card, same family as Connect
     Discord/Update available — see .pane-banner above. */

  /* ── Header ── */
  header {
    position: relative;
    height: 80px;
    display: flex; align-items: center; justify-content: space-between;
    padding: 0px 15px 0px 5px;
    border-bottom: 2px solid var(--brand);
    background: var(--header-bg);
  }
  .header-left { display: flex; align-items: stretch; height: 100%; }
  .logo { display: flex; align-items: center; padding-right: 20px; margin-right: 4px; }
  .logo-mobile { display: none; }
  .logo-desktop { display: block; height: 80px; object-fit: contain; }
  .header-right { display: flex; align-items: center; gap: 10px; }

  /* Wallet summary strip — quick-glance balances, on every page. Lives
     inside <main> (not a separate full-width bar above everything), sized
     and coloured to match the sidebar's Active Watches/Alerts row (.sb-stat)
     so the two read as one continuous band — no extra page height, and the
     sidebar's own border-right still divides them. */
  /* Height matches .sb-stat's own rendered height exactly (61px), so this
     band and the sidebar's Active Watches/Alerts row line up pixel for
     pixel — see .sb-stat below, keep the two in sync if either changes. */
  .wallet-strip { height: 61px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 20px; background: var(--sidebar-bg); border-bottom: 1px solid var(--border); flex-shrink: 0; overflow-x: auto; position: sticky; top: 0; z-index: 20; }
  /* Affiliates get a red hand-coins circle left of the tier — same height as
     the wallet pill (18px icon + 2x10px padding + 2px border), links to Referrals. */
  .wallet-strip-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
  .wallet-strip-aff { flex-shrink: 0; width: 40px; height: 40px; padding: 0; display: flex; align-items: center; justify-content: center; background: var(--bg); border: 1px solid var(--border); border-radius: 50%; color: var(--brand); cursor: pointer; transition: border-color .15s; }
  .wallet-strip-aff svg { width: 18px; height: 18px; }
  .wallet-strip-aff:hover { border-color: var(--brand); }
  .wallet-strip-tier { font-size: 12px; font-weight: 400; color: var(--text-secondary); white-space: nowrap; flex-shrink: 0; }
  .wallet-strip-tier-val { font-weight: 700; color: var(--brand); }
  .wallet-strip-right { display: flex; align-items: center; gap: 12px; }
  /* Basket total, floating next to the wallet stats pill (Luke: "move
     the basket total up next to the wallet strip") — visible on every
     page, not just Shop, so a pending basket isn't forgotten; hidden
     entirely at 0 items (see renderShopBasketPill) rather than showing
     "0" as permanent clutter on pages unrelated to shopping. */
  .wallet-strip-basket { display: flex; align-items: center; gap: 7px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-pill); padding: 10px 16px; font-size: 12px; font-weight: 600; color: var(--text); cursor: pointer; white-space: nowrap; flex-shrink: 0; box-shadow: var(--shadow-sm); font-family: inherit; }
  /* Author styles beat the UA [hidden]{display:none} rule at equal
     specificity, so the plain `display:flex` above would otherwise win
     even while hidden — this makes hidden actually hide it. */
  .wallet-strip-basket[hidden] { display: none; }
  .wallet-strip-basket:hover { border-color: var(--muted); }
  .wallet-strip-basket .amt { color: var(--brand); }
  .wallet-strip-pill { display: flex; align-items: center; gap: 15px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-pill); padding: 10px 20px; font-size: 12px; font-weight: 600; color: var(--text); cursor: pointer; white-space: nowrap; flex-shrink: 0; box-shadow: var(--shadow-sm); }
  .wallet-strip-pill { transition: border-color .15s; }
  .wallet-strip-pill:hover { border-color: var(--muted); }   /* same hover as the basket button */
  .wallet-strip-pill > .ic { display: flex; color: var(--muted); }
  .wallet-strip-pill .ic svg { width: 18px; height: 18px; }
  .wallet-item { display: flex; align-items: center; gap: 4px; border-radius: 6px; }
  .wallet-item[title="Boosts"] { gap: 2px; }
  .wallet-item.pulse-up { animation: wallet-pulse-up 900ms ease-out; }
  .wallet-item.pulse-down { animation: wallet-pulse-down 900ms ease-out; }
  /* Longer variant for a real-money purchase landing back from Stripe —
     the page reopens on whatever pane it was on before checkout, not
     necessarily Wallet, so the quick 900ms version can finish before the
     user's even looked at the header. Same keyframes, just stretched. */
  .wallet-item.pulse-up-long { animation: wallet-pulse-up 2600ms ease-out; }
  @keyframes wallet-pulse-up {
    0% { background: transparent; transform: scale(1); }
    28% { background: var(--green-bg); color: var(--green); transform: scale(1.14); }
    100% { background: transparent; transform: scale(1); }
  }
  @keyframes wallet-pulse-down {
    0% { background: transparent; transform: scale(1); }
    28% { background: var(--amber-bg); color: var(--amber); transform: scale(1.14); }
    100% { background: transparent; transform: scale(1); }
  }
  @media (prefers-reduced-motion: reduce) {
    .wallet-item.pulse-up, .wallet-item.pulse-down, .wallet-item.pulse-up-long { animation: none; }
  }
  /* Credits/tokens/boosts all stay visible on mobile too (nothing hidden) —
     just compressed: tighter padding/gaps and smaller icons so the whole
     strip fits without relying on .wallet-strip's own horizontal scroll.
     .wallet-strip's own height (61px) is NOT touched here — it's
     deliberately kept in sync with .sb-stats' row height, see the comment
     above .wallet-strip's own rule. */
  @media (max-width: 800px) {
    .wallet-strip { padding: 0 12px; gap: 8px; }
    .wallet-strip-tier { font-size: 11px; }
    .wallet-strip-right { gap: 6px; }
    .wallet-strip-basket { gap: 5px; padding: 7px 10px; font-size: 11px; }
    .wallet-strip-pill { gap: 9px; padding: 7px 12px; font-size: 11px; }
    .wallet-strip-aff { width: 34px; height: 34px; }
    .wallet-strip-pill .ic svg { width: 15px; height: 15px; }
  }
  .wallet-item .ic { display: flex; color: var(--muted); }

  /* Desktop right controls */
  .desktop-controls { display: flex; align-items: center; gap: 10px; }

  /* Username dropdown */
  .user-avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--brand); color: var(--brand-contrast); font-size: 13px; font-weight: 600; text-transform: uppercase; display: flex; align-items: center; justify-content: center; flex-shrink: 0; overflow: hidden; }
  .user-avatar.lg { width: 36px; height: 36px; font-size: 15px; }
  .user-avatar img { width: 100%; height: 100%; object-fit: cover; }
  /* Matches home.css's .user-chip (same pill shape/proportions — tight
     padding on the avatar side, more generous on the text side, larger
     avatar) so the "you're logged in" button reads as the same control
     across the marketing site and the app, not two different styles. */
  .user-btn { background: none; border: 1px solid var(--border2); border-radius: 999px; padding: 4px 14px 4px 4px; font-size: 12px; color: var(--text); font-family: var(--font); font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 8px; transition: border-color .15s; white-space: nowrap; }
  .user-btn:hover { border-color: var(--muted); }
  .user-dropdown { display: none; position: absolute; top: 80px; right: 20px; width: 210px; background: var(--menu-bg); border: 1px solid var(--border2); border-radius: var(--radius-lg); box-shadow: 0 8px 24px rgba(22,24,29,.16); z-index: 300; overflow: hidden; }
  .user-dropdown.open { display: block; }
  .user-dd-header { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-bottom: 1px solid var(--border); }
  .user-dd-header .name { font-size: 13px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .user-dd-header .sub { font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .user-dd-sep { height: 1px; background: var(--border); margin: 4px 0; }
  .user-dd-items { padding: 5px; }
  .user-dd-item { padding: 9px 10px; border-radius: 7px; font-size: 12px; cursor: pointer; color: var(--text); text-decoration: none; transition: background .12s; display: flex; align-items: center; gap: 8px; }
  .user-dd-item:hover { background: var(--surface2); }
  .user-dd-item.danger { color: var(--red); }

  /* Sound button */

  /* Mobile burger */
  .mob-burger { display: none; background: none; border: 1px solid var(--border2); border-radius: var(--radius); padding: 5px 12px; font-size: 16px; color: var(--muted); cursor: pointer; }

  /* Mobile dropdown menu */
  .mob-menu { display: none; position: absolute; top: 80px; right: 0; width: 220px; background: var(--menu-bg); border: 1px solid var(--border2); border-bottom-left-radius: 12px; box-shadow: -4px 8px 24px rgba(22,24,29,.16); z-index: 300; overflow: hidden; }
  .mob-menu.open { display: block; }

  /* ── Sidebar ── */
  aside {
    position: fixed; top: 80px; left: 0; bottom: 0; width: 260px; z-index: 40;
    background: var(--sidebar-bg);
    border-right: 1px solid var(--border);
    display: flex; flex-direction: column;
    overflow-y: auto;
    /* iPad Safari bug, Tom 2026-09-29: a watch card's two side-by-side
       .wcm-pills (Buy/Negotiate %) can be juuust wide enough to not fit
       this fixed 260px column once Safari's classic (space-consuming)
       scrollbar is in play — a real scrollbar (trackpad-connected iPad)
       eats into the available width in a way Chrome's overlay scrollbar
       never does. Without this, that overflow leaks out of aside (which
       is position:fixed, so it was never inside main's own
       overflow-x:hidden) and Safari counts it toward the whole
       document's scrollable width, producing a page-wide horizontal
       scrollbar. Chrome never showed this because it doesn't extend a
       fixed element's contribution to page scroll size the same way.
       overflow-x:hidden here just clips it at the source instead —
       paired with .wcm-pills' own flex-wrap fix so nothing is actually
       lost, just wraps to a second line rather than overflowing. */
    overflow-x: hidden;
    transition: transform .18s ease;
  }
  /* Sidebar's real content sits in this inner wrapper (not directly in
     <aside>) purely so it can be hidden independently of aside's own
     box — see the collapsed-state rule below: aside itself only ever
     translates (its width/content never resize), so without this, the
     surviving on-screen sliver would show a cropped-off edge of the
     stats grid/watch list instead of clean empty background. */
  .sidebar-inner { display: flex; flex-direction: column; flex: 1; min-height: 0; }
  /* Desktop-only collapse (see toggleDesktopSidebar) — state read from
     localStorage before first paint (pre-paint script in <head>, same
     pattern as the theme toggle) to avoid a flash of the wrong state.
     The panel slides via transform (its own width never changes) rather
     than shrinking width to 0 — no internal reflow to look janky
     mid-transition. Collapsed state leaves a 24px sliver on-screen (not
     a full -100% slide) so there's still a visible edge for the handle
     to sit on, matching how it straddles the border when open. The
     handle is a separate fixed element, not a child of <aside>, so it
     doesn't move with it — it just relocates between two resting
     points, straddling whichever edge is currently showing. */
  .sidebar-handle { display: none; }
  @media (min-width: 801px) {
    /* overflow:hidden while collapsed — aside's scrollbar sits at its own
       right edge, which is exactly the 24px sliver left on-screen for the
       handle, so without this the scrollbar was the one thing that didn't
       slide away. Content is already invisible/non-interactive here
       (.sidebar-inner opacity:0 + pointer-events:none below), so there's
       nothing to actually scroll while collapsed — scrollTop is preserved
       and overflow-y:auto just resumes normally once expanded again. */
    html.sidebar-collapsed aside { transform: translateX(calc(-100% + 24px)); overflow: hidden; }
    html.sidebar-collapsed main { margin-left: 24px; }
    html.sidebar-collapsed .sidebar-inner { opacity: 0; pointer-events: none; }
    .sidebar-handle {
      display: flex; align-items: center; justify-content: center;
      position: fixed; top: 50%; left: 249px; z-index: 41;
      width: 22px; height: 40px; margin-top: -20px;
      background: var(--surface); border: 1px solid var(--border2);
      border-radius: var(--radius);
      color: var(--muted); cursor: pointer; box-shadow: var(--shadow-sm);
      transition: left .18s ease;
    }
    .sidebar-handle:hover { color: var(--text); }
    .sidebar-handle svg { transition: transform .18s ease; }
    html.sidebar-collapsed .sidebar-handle { left: 13px; }
    html.sidebar-collapsed .sidebar-handle svg { transform: rotate(180deg); }
  }
  .sb-stats { display: grid; grid-template-columns: 1fr 1fr; border-bottom: 1px solid var(--border); }
  .sb-stat { min-height: 61px; padding: 14px 10px; display: flex; align-items: center; justify-content: center; gap: 8px; cursor: pointer; }
  .sb-stat:hover { background: var(--surface2); }
  .sb-stat:first-child { border-right: 1px solid var(--border); }
  .sb-stat-val { font-size: 30px; font-weight: 700; line-height: 1; color: var(--brand); margin: 0; padding: 0; }
  .sb-stat-label { font-size: 12px; color: var(--text-secondary); line-height: 1.25; max-width: 52px; }
  .sidebar-section { padding: 16px; border-bottom: 1px solid var(--border); }
  .sidebar-label { font-size: 11px; color: var(--muted); letter-spacing: .08em; text-transform: uppercase; margin-bottom: 10px; }

  .watch-card {
    display: flex; align-items: stretch;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-bottom: 6px;
    cursor: pointer;
    overflow: hidden;
    transition: border-color .15s;
    box-shadow: var(--shadow-sm);
  }
  .watch-card:hover { border-color: var(--border2); }
  .watch-card.active { border-color: var(--brand); }
  .watch-card.has-buy { border: 1.5px solid var(--green-border); }
  .watch-card.has-negotiate { border: 1.5px solid var(--amber-border); }
  .wc-img { width: 76px; flex-shrink: 0; background: var(--surface2); padding: 5px; display: flex; align-items: stretch; }
  .wc-img img { width: 100%; object-fit: cover; object-position: top; display: block; border-radius: 3px; }
  .wc-img-empty { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
  .wc-info { flex: 1; padding: 8px 10px; background: var(--surface2); display: flex; flex-direction: column; min-width: 0; }
  .wc-delete { background: none; border: none; cursor: pointer; color: var(--dim); padding: 0; font-size: 15px; line-height: 1; transition: color .15s; flex-shrink: 0; margin-top: -1px; }
  .wc-delete:hover { color: var(--red); }
  .watch-name { font-size: 12px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 2px; }
  .watch-set { font-size: 11px; color: var(--muted); margin-bottom: 6px; }
  .watch-footer { display: flex; align-items: center; justify-content: space-between; }
  .status-badge { font-size: 11px; padding: 2px 7px; border-radius: 10px; }
  .status-badge.running { background: var(--green-bg); color: var(--green); border: 1px solid var(--green-border); }
  .status-badge.idle { background: var(--blue-bg); color: var(--blue); border: 1px solid rgba(59,158,255,.25); }
  .status-badge.paused { background: var(--surface2); color: var(--muted); border: 1px solid var(--border); }
  .status-badge.error { background: var(--red-bg); color: var(--red); border: 1px solid rgba(231,76,60,.25); }
  .watch-mkt { font-size: 11px; color: var(--muted); }

  .wc-timer { font-size: 11px; color:var(--dim); display:flex; align-items:center; gap:3px; transition:color .2s; }
  .wc-timer.scanning { color:var(--green); animation:timer-pulse .9s ease-out; }
  @keyframes timer-pulse { 0%{opacity:1} 40%{opacity:.35} 100%{opacity:1} }

  /* ── Compact sidebar watch card (wcm) — replaces .watch-card everywhere,
     including the upgrade-tier promo slot below. ── */
  .wcm { display: flex; gap: 10px; padding: 10px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-nested); margin-bottom: 6px; cursor: pointer; box-shadow: var(--shadow-sm); transition: border-color .15s; }
  .wcm:hover { border-color: var(--border2); }

  /* Sidebar's narrow column gets its own stacked layout rather than reusing
     .dash-upgrade's wide horizontal one verbatim — same visual language
     (dashed tinted border, diamond icon badge) adapted to fit a ~230px
     column instead of overflowing/wrapping it. .elite swaps the palette for
     the no-upsell "thanks" variant (no CTA, nothing to sell). */
  /* Sidebar "Upgrade to X" tile — layered on top of the base .wcm row
     (same layout: icon in the standard 84px .wcm-art box on the left,
     title/sub vertically centered to the right) so it's exactly the same
     height as a real watch card, same pattern as .wcm.wcm-add. */
  .wcm-upgrade { border: 1.5px dashed var(--brand-border); background: var(--brand-bg); }
  .wcm-upgrade-art { background: rgba(232,37,26,.12); color: var(--brand); }
  .wcm-upgrade.elite { cursor: default; border-color: var(--green-border); background: var(--green-bg); }
  .wcm-upgrade.elite .wcm-upgrade-art { background: rgba(18,160,91,.12); color: var(--green); }
  .wcm-artwrap { position: relative; flex-shrink: 0; }
  .wcm-art { height: 84px; aspect-ratio: 5/7; border-radius: 4px; border: 1px solid var(--border); background: var(--surface2); overflow: hidden; display: flex; align-items: center; justify-content: center; }
  .wcm-art img { width: 100%; height: 100%; object-fit: cover; object-position: top; }
  .wcm-art.custom { border-style: dashed; }
  .wcm-corner { position: absolute; top: -6px; left: -6px; border-radius: var(--r-pill); padding: 4px 8px; display: flex; align-items: center; gap: 1px; border: 1.5px solid var(--surface); }
  .wcm-corner svg { display: block; }
  .wcm-corner.add-boost { background: var(--surface); border: 1px solid var(--boost-border); cursor: pointer; }
  .wcm-corner.add-boost .b { display: flex; color: rgba(106,92,214,.4); }
  .wcm-corner.add-boost .p { display: flex; color: var(--boost); }
  .wcm-corner.boosted { background: var(--boost); color: #fff; }
  .wcm-corner.turbo { background: var(--boost-deep); color: #fff; }
  .wcm-corner.turbo svg + svg { margin-left: -4px; }
  /* Charge badge — graduated blue bolts (each darker), + until maxed at 4 */
  .wcm-corner.charge, .wc2-corner.charge { background: var(--surface); border: 1px solid var(--blue-border); color: var(--blue); gap: 0; }
  .wcm-corner.charge svg, .wc2-corner.charge svg { color: var(--blue); }
  .wcm-corner.charge:not(.maxed), .wc2-corner.charge:not(.maxed) { cursor: pointer; }
  .wcm-corner.charge.maxed, .wc2-corner.charge.maxed { cursor: default; }
  .wcm-corner.charge.loading, .wc2-corner.charge.loading { pointer-events: none; opacity: .6; }
  .wcm-corner.charge.loading svg:not(.spinner), .wc2-corner.charge.loading svg:not(.spinner) { display: none; }
  .wd-plus.loading { pointer-events: none; opacity: .6; }
  .charge .spinner, .wd-plus .spinner, .aw-searchfield .spinner, .aw-start-btn .spinner, .search-market-btn .spinner, .shop-buy-btn .spinner { animation: spin .7s linear infinite; }
  @keyframes spin { to { transform: rotate(360deg); } }
  @media (prefers-reduced-motion: reduce) { .charge .spinner, .wd-plus .spinner, .aw-searchfield .spinner, .aw-start-btn .spinner, .search-market-btn .spinner, .shop-buy-btn .spinner { animation: none; } }
  .wcm-info { flex: 1; min-width: 0; display: flex; flex-direction: column; }
  .wcm-name { font-size: 12.5px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .wcm-sub { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
  .wcm-price { font-size: 14px; font-weight: 600; color: var(--text); margin-top: 2px; }
  .wcm-price .l { font-size: 11px; font-weight: 400; color: var(--muted); }
  .wcm-price.dim { color: var(--muted); font-weight: 400; font-size: 11px; }
  .wcm-pills { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
  .wcm-pill { font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: var(--r-pill); white-space: nowrap; }
  .wcm-pill.buy { background: var(--green-bg); color: var(--green); border: 1px solid var(--green-border); }
  .wcm-pill.neg { background: var(--amber-bg); color: var(--amber); border: 1px solid var(--amber-border); }
  .wcm.wcm-add { background: var(--surface2); border: 1px dashed var(--border2); box-shadow: none; }
  .wcm.wcm-add:hover { background: var(--surface); border-color: var(--brand-border); }
  .wcm.wcm-add .wcm-art { background: var(--brand-bg); border: 1px dashed var(--brand-border); color: var(--brand); }
  .wcm.wcm-add:hover .wcm-art { border-color: var(--brand); }

  /* Market-price pulse icon (PokePulse logo) shown next to a live price */
  .market-price-icon { width: 14px; height: 14px; flex: 0 0 14px; opacity: .9; vertical-align: -2px; }
  .wcm-price:not(.dim), .wc2-price:not(.dim) { display: flex; align-items: center; }
  .wcm-price:not(.dim) { gap: 4px; }
  .wc2-price:not(.dim) { gap: 5px; }

  /* ── Watches tab card (v2 layout) — isolated from .watch-card, which the
     sidebar's compact watch list and the "Go Premium" card also use ── */
  /* min-width:0 overrides the grid item's default min-width:auto — without
     it, .watches-grid's 1fr columns don't actually cap a card's width once
     .wc2-sub (below) can no longer wrap to relieve its min-content size;
     the whole card (and page) would overflow sideways instead of the
     truncation inside it ever triggering. */
  .wc2-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px; cursor: pointer; transition: border-color .15s; box-shadow: var(--shadow-sm); min-width: 0; }
  .wc2-card:hover { border-color: var(--border2); }
  .wc2-card.has-buy { border: 1.5px solid var(--green-border); }
  .wc2-card.has-negotiate { border: 1.5px solid var(--amber-border); }
  .wc2-card.inactive { opacity: .7; }
  .wc2-top { display: flex; gap: 12px; }
  .wc2-artwrap { position: relative; flex-shrink: 0; }
  /* "Combo" premium effect (Luke: make the card thumbnail "look like a
     million dollars" — glow + shine sweep + sparkle), demoed as a set of
     options on localhost before landing here. Add Watch's "Your selection"
     and Watch Detail's hero only — Luke: "We don't want the glow in the
     watch list" — so scoped to wrappers that hold the bigger `.wd-art` hero
     image, never the watch-list card's own `.wc2-art`. Revised per Luke's
     second round of feedback: the glow is a box-shadow pulse (a blurred
     radial-gradient halo read as "nothing" — too faint to notice); the
     sparkles get a dark ring behind their white/brand core (plain white on
     `mix-blend-mode:screen` vanished against the card's own light border);
     the shine is a single monochrome sweep, not a rainbow holo band, and
     loops via `animation-direction:alternate` so it eases back the way it
     came each cycle instead of snapping to a reset position — no bob
     (Luke: it read as the effect "only moving vertically"). */
  /* Luke, 2026-10-26: DISABLED for testing — reproduces in real Safari too
     (not just iOS Chrome/WebKit in general), coincided with a recent iOS
     update, and hits BOTH this page (.wc2-art, shouldn't match :has(.wd-art)
     at all) and Watch Detail's hero (.wd-art, the intended target). The
     shared thread is this :has() rule itself, keyed off the .wc2-artwrap
     class both pages use — :has() invalidation is one of the trickiest
     parts of the spec to implement correctly, and a WebKit regression
     there could plausibly leak this rule onto .wc2-artwrap elements that
     never should have matched it. Re-enable once confirmed/fixed; keep
     the @keyframes (Add Watch's own #aw-market-result scope may still
     reference them). */
  /*
  .wc2-artwrap:has(.wd-art) .wd-art {
    position: relative; z-index: 1;
    animation: wcGlowPulse 2.4s ease-in-out infinite;
  }
  .wc2-artwrap:has(.wd-art)::after {
    content: ""; position: absolute; inset: 0; border-radius: 7px; overflow: hidden;
    z-index: 2; pointer-events: none;
    background: linear-gradient(100deg, transparent 42%, rgba(255,255,255,.3) 50%, transparent 58%) 0 0 / 260% 260% no-repeat;
    animation: wcShineSweep 4.5s ease-in-out infinite alternate;
  }
  @media (prefers-reduced-motion: reduce) {
    .wc2-artwrap:has(.wd-art) .wd-art, .wc2-artwrap:has(.wd-art)::after { animation: none; }
  }
  */
  @keyframes wcGlowPulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(232,37,26,0), 0 0 0 0 rgba(232,37,26,0); }
    50% { box-shadow: 0 0 12px 2px rgba(232,37,26,.45), 0 0 27px 7px rgba(232,37,26,.22); }
  }
  @keyframes wcShineSweep {
    from { background-position: -90% -60%; }
    to   { background-position: 50% 40%; }
  }
  .wc2-art { height: 100px; aspect-ratio: 5/7; border-radius: 4.5%/3.5%; border: 1px solid var(--border); background: var(--surface); display: flex; align-items: center; justify-content: center; overflow: hidden; }
  .wc2-art img { width: 100%; height: 100%; object-fit: cover; object-position: top; }
  .wc2-art.custom { border-style: dashed; }
  .wc2-corner { position: absolute; top: -6px; left: -6px; z-index: 3; border-radius: var(--r-pill); height: 20px; padding: 0 5px; display: flex; align-items: center; gap: 2px; border: 1.5px solid var(--surface2); font-size: 11px; }
  .wc2-corner.add-boost { background: var(--surface2); border-color: var(--boost-border); color: var(--boost); cursor: pointer; opacity: .8; }
  .wc2-corner.add-boost:hover { opacity: 1; }
  .wc2-corner.boosted { background: var(--boost); color: #fff; }
  .wc2-corner.turbo { background: var(--boost-deep); color: #fff; }
  .wc2-corner.status { background: var(--dim); padding: 0 7px; cursor: default; }
  .wc2-corner.status span { font-size: 11px; font-weight: 500; color: var(--bg); letter-spacing: .2px; }
  .wc2-info { flex: 1; min-width: 0; }
  .wc2-name-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
  /* font-size !important here, not just specificity: the markup also
     carries the shared .card_title class (font-size:14px !important,
     above) — Luke, 2026-10-26: scoped this size bump to the Watches-list
     card specifically rather than bumping .card_title/.card_sub_title
     globally (used elsewhere too: win-card hero, modals, shop basket). */
  .wc2-name { font-size: 16px !important; line-height: 1.3; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .wc2-name.dim { color: var(--muted); }
  /* Luke, 2026-10-26: "that needs to truncate and not wrap" — the Number ·
     Set · Material · Promo line could run to two lines once enough parts
     are present, pushing the price/pills row down inconsistently between
     cards; truncates to one line with an ellipsis instead. */
  .wc2-sub { font-size: 13px !important; color: var(--muted); margin-top: 2px; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .wc2-price { display: flex; align-items: baseline; gap: 4px; margin-top: 7px; }
  .wc2-price .v { font-size: 16px; font-weight: 600; color: var(--text); }
  .wc2-price .l { font-size: 11px; color: var(--muted); }
  .wc2-price.dim .v { color: var(--muted); }
  .wc2-nomarket { display: flex; align-items: center; gap: 5px; margin-top: 8px; font-size: 11px; color: var(--muted); }
  .wc2-pills { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
  .wc2-pill { font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 20px; white-space: nowrap; }
  .wc2-pill.buy { background: var(--green-bg); color: var(--green); border: 1px solid var(--green-border); }
  .wc2-pill.neg { background: var(--amber-bg); color: var(--amber); border: 1px solid var(--amber-border); }
  .wc2-pill.off { background: var(--surface); color: var(--muted); border: 1px solid var(--border); }
  .wc2-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; border-top: 1px solid var(--border); margin-top: 10px; padding-top: 8px; font-size: 11px; color: var(--muted); }
  .wc2-foot .right { display: flex; align-items: center; gap: 14px; }
  .wc2-foot .unlim { display: inline-flex; align-items: center; gap: 3px; }
  .wc2-foot .soon { color: var(--amber); }
  /* A purchased watch's remaining time is worth noticing, not just a footnote
     — but every other color here is already claimed (amber/red = Buy/
     Negotiate and the ChaseCard brand red, blue/purple = Boosts and
     PokePulse), so this stays neutral rather than fight those for attention.
     The label itself still switches from days to hours inside the urgent
     window; only the pill's color no longer escalates. */
  .wc2-countdown { font-weight: 600; padding: 2px 8px; border-radius: 9px; background: var(--bg); color: var(--text); }
  .wc2-renew { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 500; padding: 3px 9px; border-radius: 8px; border: 1px solid var(--boost-border); background: var(--surface); color: var(--boost); cursor: pointer; font-family: inherit; }

  /* ⋯ menu */
  .wc2-menu { position: relative; flex-shrink: 0; }
  .wc2-menu-trigger { border: none; background: none; padding: 2px 4px; border-radius: 6px; cursor: pointer; color: var(--muted); line-height: 0; }
  .wc2-menu-trigger:hover { background: var(--surface); color: var(--text); }
  .wc2-menu.open .wc2-menu-trigger { background: var(--surface); color: var(--text); }
  .wc2-menu-pop { position: absolute; top: 26px; right: 0; width: 178px; background: var(--surface2); border: 1px solid var(--border2); border-radius: var(--r-control); box-shadow: 0 10px 28px -8px rgba(22,24,29,.16); padding: 5px; z-index: 30; display: none; }
  .wc2-menu.open .wc2-menu-pop { display: block; }
  .wc2-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 9px; border-radius: 6px; border: none; background: none; font-size: 12px; color: var(--text); cursor: pointer; text-align: left; font-family: inherit; }
  .wc2-menu-item:hover { background: var(--surface); }
  .wc2-menu-item.danger { color: var(--red); }
  .wc2-menu-sep { height: 1px; background: var(--border); margin: 4px 5px; }

  /* Luke, 2026-10-26: Watches-list card redesign — "a bigger pokemon
     card image, showing the same data and a better use of space." Luke,
     2026-10-26 (follow-up): "this is the same layout I want across all
     views of this page, on desktop also" — originally scoped to the
     ≤500px mobile tier only, now unconditional at every width. The
     bigger thumbnail (100px → 140px tall, aspect-ratio keeps it
     proportional) needs everything else tightened to still land at a
     slightly SHORTER card overall than before, per spec. */
  .wc2-card { padding: 10px; }
  .wc2-top { gap: 10px; }
  /* Luke, 2026-10-26: explicit width alongside the aspect-ratio-derived
     one (140px height × 5/7 = 100px) — WebKit 27.0's own release notes
     (installed on Luke's iPhone the day before this bug was reported)
     list a cluster of fixes bundled together matching this exact shape:
     "min-width: auto not correctly computed for flex items", "aspect-
     ratio not preserved on images with width:100%/height:100%", "flex
     containers with aspect-ratio-derived height not providing a
     definite cross size to their flex items" — .wc2-art is exactly that
     (aspect-ratio-derived width, inside a flex row, holding a
     width:100%/height:100% img). An explicit width removes the
     ambiguity those fixes changed the behavior of, rather than relying
     on the engine to derive it correctly. .wc2-artwrap gets the same
     treatment below so it can't inherit any different implicit sizing. */
  .wc2-art { height: 140px; width: 100px; }
  .wc2-artwrap { width: 100px; }
  /* "squarer rounded boost panel" — pill (var(--r-pill)) down to a
     tighter rounded-square, same top-left position over the thumbnail
     (confirmed with Luke: position stays, just the corner radius). */
  .wc2-corner { border-radius: 8px; }
  /* "tucks the dot/burger menu up tighter in the top right" — trims the
     trigger's own hit-area padding so it sits closer to the card's
     actual corner instead of floating inside extra click-padding. */
  .wc2-menu-trigger { padding: 0 2px; }
  /* Luke, 2026-10-26: "the card name should be a little larger and the
     line heights a little taller so allow a little more space" — name's
     own font-size/line-height bumped above; these margins (tightened for
     the redesign above) loosened back up slightly to match. */
  .wc2-name-row { margin-top: 5px; }
  .wc2-sub { margin-top: 6px; line-height: 1.45; }
  .wc2-price { margin-top: 6px; }
  .wc2-pills { margin-top: 9px; }
  .wc2-foot { margin-top: 10px; padding-top: 8px; }

  .wc2-add { background: var(--surface2); border: 1px dashed var(--border2); border-radius: var(--radius-lg); padding: 10px; cursor: pointer; width: 100%; text-align: left; font-family: inherit; display: block; box-shadow: none; transition: border-color .15s, background .15s; }
  .wc2-add:hover { background: var(--surface); border-color: var(--brand-border); }
  .wc2-add .wc2-art { background: var(--brand-bg); border: 1px dashed var(--brand-border); border-style: dashed; display: flex; align-items: center; justify-content: center; }
  .wc2-add:hover .wc2-art { border-color: var(--brand); }
  .wc2-add .ic { display: flex; align-items: center; justify-content: center; color: var(--brand); }
  .wc2-add .wc2-info { display: flex; flex-direction: column; justify-content: center; }
  .wc2-add .t { font-size: 13px; font-weight: 600; color: var(--text); }
  .wc2-add .h { font-size: 11px; color: var(--muted); margin-top: 2px; }

  /* Free-tier watch-limit signpost banner (Tom, 2026-09-23/24) — the
     tutorial's purple animated border-beam (.glow-border.tutorial-purple,
     defined further down) applied permanently rather than for a tour's
     duration. Single-row bar (icon | text | actions), per Tom's mockup —
     was a tall centered card before 2026-09-24's redesign. */
  .watch-limit-banner { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); display: flex; align-items: center; gap: 18px; padding: 16px 22px; }
  .watch-limit-banner-icon { width: 56px; height: 56px; border-radius: 14px; background: rgba(var(--tutorial-accent-rgb),.12); color: var(--tutorial-accent); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
  .watch-limit-banner-text { flex: 1; min-width: 0; }
  .watch-limit-banner-title { font-size: 18px; font-weight: 700; color: var(--text); }
  .watch-limit-banner-sub { font-size: 14px; color: var(--muted); line-height: 1.5; margin-top: 2px; }
  .watch-limit-banner-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
  .watch-limit-banner-or { font-size: 12px; color: var(--muted); }
  /* Buy Credits and View plans read as equal-weight options — both share
     .submit-btn (identical padding/font/radius) and are overridden here
     to the same white-outline look (Tom, 2026-09-24, refining Luke's
     same-day solid-purple-fill attempt: "the text and border should be
     white. make the hover state the darker"). Hover darkens instead of
     the plain .submit-btn opacity dim. */
  .watch-limit-banner-actions .submit-btn { background: transparent; color: var(--text); border: 1px solid var(--text); }
  .watch-limit-banner-actions .submit-btn:hover { opacity: 1; background: rgba(0,0,0,.12); }
  /* Same eye glyph the Watches page heading itself uses (Luke, 2026-09-24:
     "make it look more like the watches eye icon") — a plain animated SVG,
     not a third-party Lottie file, so the shape is guaranteed to match
     exactly. Blinks (vertical squash) periodically rather than looping
     continuously — reads as "watching", not distracting. */
  .watch-limit-eye-icon { animation: watch-limit-eye-blink 3.4s ease-in-out infinite; transform-origin: center; }
  @keyframes watch-limit-eye-blink {
    0%, 88%, 100% { transform: scaleY(1); }
    91% { transform: scaleY(.1); }
    94% { transform: scaleY(1); }
  }
  @media (prefers-reduced-motion: reduce) { .watch-limit-eye-icon { animation: none; } }
  @media (max-width: 700px) {
    .watch-limit-banner { flex-direction: column; align-items: flex-start; }
    .watch-limit-banner-actions { width: 100%; }
    .watch-limit-banner-actions .submit-btn { flex: 1; }
  }
  /* Hidden for the tour's duration (Luke, 2026-09-24) — its own "buy
     credits/upgrade" pitch has nothing to do with the tutorial and would
     otherwise sit in the watches grid the tour is narrating. Pure CSS
     keyed off the same body.tutorial-active class startTutorial()/
     _tutorialFinish() already toggle, rather than JS bookkeeping to
     remember and restore it — renderDashboard() (called at tour end)
     naturally re-renders it from the real signpost condition regardless
     of whether it was showing before the tour started. */
  body.tutorial-active .watch-limit-banner { display: none !important; }
  /* The tour's sample alert: its Buy now / Make an offer button must never be clickable. Done in CSS (not by stripping the href once, as
     _tutorialInjectFakeAlerts also does) because any later re-render of the Alerts list rebuilds the card with a real /go link. */
  body.tutorial-active [id^="alert-tutorial-fake"] .alert-cta { pointer-events: none; cursor: default; }

  /* Tom, 2026-09-23: "make the new banner do a little shake ... before
     you can add a watch" — nudged from _shakeWatchLimitBanner() when the
     Add-a-watch tile is clicked while this banner needs actioning first. */
  @keyframes watch-limit-banner-shake {
    0%, 100% { transform: translateX(0); }
    20% { transform: translateX(-6px); }
    40% { transform: translateX(5px); }
    60% { transform: translateX(-4px); }
    80% { transform: translateX(3px); }
  }
  .watch-limit-banner.shake { animation: watch-limit-banner-shake .4s ease-in-out; }
  @media (prefers-reduced-motion: reduce) { .watch-limit-banner.shake { animation: none; } }

  /* Larger, more rounded, with a message-relevant icon+color — Luke,
     2026-09-02: "these do need to be a little more obvious they are
     often lost... a little larger with more padding and rounded radii." */
  .cc-toast { position: fixed; left: 50%; bottom: 32px; transform: translateX(-50%) translateY(8px); display: flex; align-items: center; gap: 10px; min-width: 270px; background: var(--surface2); border: 1px solid var(--border2); color: var(--text); font-size: 13px; font-weight: 500; padding: 13px 20px; border-radius: 16px; box-shadow: var(--shadow-lg); opacity: 0; transition: opacity .18s, transform .18s; z-index: 500; pointer-events: none; }
  .cc-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
  .cc-toast-icon { flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
  .cc-toast-icon svg { width: 13px; height: 13px; }
  .cc-toast-icon.brand { background: var(--brand-bg); color: var(--brand); }
  .cc-toast-icon.boost { background: var(--boost-bg); color: var(--boost); }
  @keyframes best-win-card-fire {
    0%,100% { box-shadow:0 0 7px 2px rgba(255,100,0,.65),0 0 18px rgba(255,60,0,.3);  border-color:rgba(255,130,0,.85); }
    33%      { box-shadow:0 0 12px 3px rgba(255,210,0,.75),0 0 26px rgba(255,150,0,.4); border-color:rgba(255,215,0,.95); }
    66%      { box-shadow:0 0 9px 2px rgba(255,50,0,.7), 0 0 20px rgba(255,80,0,.3);   border-color:rgba(255,80,0,.9); }
  }
  @keyframes best-win-inner-glow {
    0%,100% { box-shadow:inset 0 0 18px rgba(255,100,0,.09); }
    50%      { box-shadow:inset 0 0 28px rgba(255,160,0,.16); }
  }
  @keyframes best-win-featured-glow {
    0%,100% { box-shadow:0 7px 22px -3px rgba(255,110,0,.3), inset 0 0 18px rgba(255,100,0,.09); }
    50%      { box-shadow:0 10px 32px -3px rgba(255,170,0,.5), inset 0 0 28px rgba(255,160,0,.16); }
  }
  .best-win-card { animation:best-win-card-fire 2.4s ease-in-out infinite; }
  .best-win-row          { animation:best-win-inner-glow    2.4s ease-in-out infinite; position:relative; }
  .best-win-row.featured { animation:best-win-featured-glow 2.4s ease-in-out infinite; }

  .btn-add {
    display: flex; align-items: center; justify-content: center; gap: 6px;
    width: 100%; padding: 8px;
    background: transparent; border: 1px dashed var(--dim);
    border-radius: var(--radius); color: var(--muted);
    cursor: pointer; font-family: inherit; font-size: 12px;
    transition: all .15s; margin-top: 4px;
  }
  .btn-add:hover { border-color: var(--border2); color: var(--text); }

  /* ── Main ── */
  /* main (not .pane) owns the vertical scrollbar. main spans the full
     remaining viewport width; .pane is a centered, max-width box inside it
     purely for content layout. Scrolling .pane instead put the scrollbar
     at the pane's own edge — visibly inset from the browser window's right
     edge on wide screens instead of flush against it. */
  main { margin-left: 260px; height: calc(100vh - 80px); display: flex; flex-direction: column; overflow-x: hidden; overflow-y: auto; transition: margin-left .18s ease; }

  .header-nav { position: absolute; left: 50%; top: 0; bottom: 0; transform: translateX(-50%); display: flex; align-items: flex-end; padding: 0 4px; }
  .tab { padding: 0 12px; height: 100%; display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color .15s; white-space: nowrap; }
  .tab.active { color: var(--brand); border-bottom-color: var(--brand); }
  /* Top-bar tab that doesn't fit the current window width — hidden here,
     shown in the account dropdown instead (see _navFitTopBar). */
  .header-nav .tab.nav-hidden { display: none; }
  .mob-nav-icons .mob-nav-btn.nav-hidden { display: none; }
  .tab:hover:not(.active) { color: var(--text); }

  .pane { display: none; flex: 1; padding: 16px; max-width: 1200px; width: 100%; margin-left: auto; margin-right: auto; }
  .pane.active { display: block; }
  @media (min-width: 801px) { .pane { padding: 10px 16px 16px 16px; } }
  /* Luke, 2026-09-24 (correcting an earlier "just hide the footer on
     mobile" attempt): "all i wanted was the page to have a minimum
     height so that the footer wasn't seen on screen in the instance that
     there was not much content on the page". A short pane (e.g. Add
     Watch before a card's picked) otherwise ends right where the footer
     begins, pulling it up into the first screenful; this keeps the
     footer itself untouched and just gives the active pane enough height
     to push it below the fold on any short page, while a pane that's
     already taller than one screen is unaffected (min-height, not a
     fixed height). 100% (of `main`'s own real content-box height, itself
     `calc(100vh - 80px)` — see main's own rule), NOT 100vh: `main` is the
     actual scroll container here (`overflow-y: auto`), shorter than the
     full viewport by its header offset, so 100vh overshot it by that
     much on every single page — landing the footer at an inconsistent
     depth instead of reliably "one screenful down" (Luke: "the footer is
     appearing in the middle of the page"). */
  @media (max-width:800px) {
    .pane.active {
      /* .pane's base rule is `flex:1` — shorthand for flex-grow:1,
         flex-shrink:1, flex-basis:0%. That 0% basis is the real culprit:
         once a page's content (e.g. Settings' full card stack) is
         already taller than main's visible height, the flex layout is in
         "negative free space", so flex-grow never engages (it only
         distributes *positive* leftover space) — the item's used size
         collapses toward its 0% basis, clamped back up only by
         min-height. An explicit min-height:100% (needed below, to make
         a SHORT page's pane reach full height) then becomes a hard
         ceiling instead of a floor: the box renders at exactly 100% while
         the real content silently overflows past it (default
         overflow:visible), landing the footer visually on top of that
         overflow instead of after it (Luke: "the footer is appearing in
         the middle of the page"). flex-basis:auto (via `flex:1 0 auto`)
         fixes the root cause — the item's basis is now its own content
         size, so a short page still gets flex-grow:1 stretching it to
         fill the screen, while a tall page's basis already exceeds the
         container and is simply left at its full content height. */
      flex: 1 0 auto;
      min-height: 100%;
    }
  }

  /* Pane banners — Luke, 2026-09-15: "we want them in a separate pane of
     their own that will live above all pages... this pane only exists IF
     the user has any banners visible within it." Same width/side-padding
     as .pane above so a banner card lines up with the page content under
     it regardless of which page that is; no bottom padding of its own —
     .pane-banner's own margin-bottom already spaces it from whatever
     follows. Collapses to zero height (not just hidden banners inside an
     empty box) until _updateBannerPaneVisibility() (dashboard.js) finds
     at least one banner actually visible and adds .has-banners. */
  #pane-banners { display: none; flex-shrink: 0; max-width: 1200px; width: 100%; margin: 0 auto; padding: 16px 16px 0; box-sizing: border-box; }
  #pane-banners.has-banners { display: block; }
  /* Held back until the rest of the page has loaded, then released (see _releaseBanners) */
  #pane-banners.banners-hold { display: none !important; }
  @media (min-width: 801px) { #pane-banners { padding: 10px 16px 0; } }

  .page-heading { min-height: 36px; display: flex; align-items: center; gap: 10px; margin-bottom: 16px; font-size: 22px; font-weight: 600; color: var(--text); }
  /* Same brand-coloured icon-before-title language as the shop's
     Merch/Competitions section headings (.shop-catalog-heading) —
     Luke liked it there and wanted it on every page heading. */
  .page-heading > svg:first-child { color: var(--brand); flex-shrink: 0; }

  /* ── Dashboard pane ── */
  .summary-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 20px; }
  .metric { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-tile); padding: 12px 14px; }
  .metric-label { font-size: 11px; color: var(--muted); margin-bottom: 6px; letter-spacing: .05em; }
  .metric-val { font-size: 22px; font-weight: 500; font-family: var(--font); }
  .metric-val.green { color: var(--green); }
  .metric-val.amber { color: var(--amber); }

  .section-head { font-size: 11px; color: var(--text); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 10px; }

  /* ── Alerts ── */
  .alert-item {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    margin-bottom: 8px;
    transition: border-color .15s;
    position: relative;
    box-shadow: var(--shadow-sm);
  }
  .alert-item.buy { border-color: rgba(46,204,113,0.3); }
  .alert-item.negotiate { border-color: rgba(240,165,0,0.25); }
  .alert-item:hover.buy { border-color: rgba(46,204,113,0.55); }
  .alert-item:hover.negotiate { border-color: rgba(240,165,0,0.45); }
  .alert-item.muted { border-color: var(--border2) !important; }
  .alert-item.gone { border-color: rgba(231,76,60,0.4) !important; }
  .alert-item.dismissed { display: none; }
  .alert-item.selected { border-color: var(--brand-border); }

  /* Swipe wrapper (mobile only) — .alert-item is the sliding surface; two independent
     .alert-swipe-bg layers sit behind it, one revealed on the right as the card slides left
     (swipe left), one on the left as it slides right (swipe right) — only ever one visible at
     a time, since a drag is either left or right, never both (Luke, 2026-09-22 spec):
       Active:   swipe left = Delete (red),  swipe right = Archive (grey)
       Archived: swipe left = Delete (red),  swipe right = Restore (green)
       Lost:     swipe left = Delete (red);  no right-swipe action at all */
  .alert-swipe-wrap { position: relative; overflow: hidden; border-radius: var(--radius-lg); margin-bottom: 8px; }
  .alert-swipe-wrap .alert-item { margin-bottom: 0; touch-action: pan-y; will-change: transform; }
  .alert-swipe-bg {
    display: none; position: absolute; inset: 0; border-radius: var(--radius-lg);
    align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: #fff;
  }
  /* Revealed on the right (swiping left) */
  .alert-swipe-bg.delete { background: var(--brand); justify-content: flex-end; padding-right: 22px; }
  /* Revealed on the left (swiping right) */
  .alert-swipe-bg.archive { background: var(--grey-fill); justify-content: flex-start; padding-left: 22px; }
  .alert-swipe-bg.restore { background: var(--green); justify-content: flex-start; padding-left: 22px; }
  /* Only shown mid-swipe, and only the layer matching the live drag direction — .alert-item.muted
     is semi-transparent, so a permanently-visible bg layer would tint every archived card. */
  @media (max-width: 800px) {
    .alert-swipe-wrap.swiping-left .alert-swipe-bg.delete { display: flex; }
    .alert-swipe-wrap.swiping-right .alert-swipe-bg.archive,
    .alert-swipe-wrap.swiping-right .alert-swipe-bg.restore { display: flex; }
  }

  .alert-row { display: flex; align-items: stretch; }
  .alert-select-rail { display: none; align-items: center; justify-content: center; width: 34px; flex-shrink: 0; cursor: pointer; }
  #all-alerts .alert-select-rail { display: flex; padding-left: 10px; }
  @media (max-width: 800px) {
    #all-alerts .alert-select-rail { display: none; }
    .select-all-alerts { display: none !important; }
  }

  .checkbox-visual {
    width: 14px; height: 14px; border: 1.5px solid var(--border2); border-radius: 4px;
    display: flex; align-items: center; justify-content: center; background: var(--surface);
    flex-shrink: 0; transition: background .15s, border-color .15s;
  }
  .checkbox-visual.lg { width: 18px; height: 18px; border-radius: 5px; }
  .checkbox-visual svg { width: 10px; height: 10px; stroke: var(--brand-contrast); }
  .checkbox-visual.checked, .checkbox-visual.indeterminate { background: var(--brand); border-color: var(--brand); }

  .select-all-alerts {
    display: flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; padding: 0;
    border: 1px solid var(--border2); border-radius: var(--radius); cursor: pointer;
    background: var(--surface2); flex-shrink: 0;
  }

  .alert-bulk-bar {
    display: flex; align-items: center; gap: 10px; padding: 0 14px;
    max-height: 0; opacity: 0; overflow: hidden; border: 1px solid transparent;
    border-radius: var(--radius-lg); margin-bottom: 0; font-size: 12px;
    transition: max-height .18s ease, opacity .15s ease, padding .18s ease, margin .18s ease, border-color .15s;
  }
  .alert-bulk-bar.show { max-height: 60px; opacity: 1; padding: 10px 14px; margin-bottom: 12px; border-color: var(--brand-border); background: var(--brand-bg); }
  .alert-bulk-bar b { color: var(--text); }
  .alert-bulk-bar-actions { margin-left: auto; display: flex; gap: 8px; }

  /* Top row: type+pct chip, badges, source/time, ⋯ menu */
  .alert-crow { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px 14px 0; }
  .alert-crow-left { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
  .alert-crow-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
  .alert-chip { font-size: 12px; font-weight: 500; padding: 3px 10px; border-radius: 20px; white-space: nowrap; }
  .alert-chip.buy { background: var(--green-bg); color: var(--green); }
  .alert-chip.negotiate { background: var(--amber-bg); color: var(--amber); }
  .alert-source { font-size: 11px; color: var(--muted); white-space: nowrap; }
  .alert-source b { color: var(--text); font-weight: 500; }

  /* Community flag ("Possible Fake"/"Possible Damage"/"Possible Error") —
     see a.flag_type in alertHTML (app_server.py's _get_community_flags
     computes it live, never stored). Same warning-triangle icon
     .alert-price-warn already uses; borderless, matching .alert-chip's
     own fill+color-only treatment, no wrapper chip. Desktop shows the
     icon plus a text label inline; mobile (<=800px, below) drops the
     label — same breakpoint every other alert-card mobile rule uses —
     and relies on the icon's native title tooltip instead, exactly like
     .alert-price-warn does. */
  .alert-flag { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 600; color: var(--red); white-space: nowrap; }
  .alert-flag.amber { color: var(--amber); }
  .alert-flag svg { flex-shrink: 0; }
  /* Fake reads as more severe than Damaged/Incorrect — a plain color
     swap on the same border-color property .alert-item.buy/.negotiate
     already use, so it composes the same way .gone already does. */
  .alert-item.flagged { border-color: rgba(255,116,108,.5) !important; }
  .alert-item.flagged.amber { border-color: rgba(246,181,98,.5) !important; }
  @media (max-width: 800px) {
    .alert-flag-label { display: none; }
  }

  .alert-menu { position: relative; flex-shrink: 0; }
  .alert-menu-trigger { border: none; background: none; padding: 2px 4px; border-radius: 6px; cursor: pointer; color: var(--muted); line-height: 0; }
  .alert-menu-trigger:hover { background: var(--surface2); color: var(--text); }
  .alert-menu.open .alert-menu-trigger { background: var(--surface2); color: var(--text); }
  .alert-menu-pop { position: absolute; top: 26px; right: 0; width: 216px; background: var(--surface2); border: 1px solid var(--border2); border-radius: var(--r-control); box-shadow: 0 10px 28px -8px rgba(22,24,29,.16); padding: 5px; z-index: 30; display: none; }
  .alert-menu.open .alert-menu-pop { display: block; }
  .alert-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 9px; border-radius: 6px; border: none; background: none; font-size: 12px; color: var(--text); cursor: pointer; text-align: left; font-family: inherit; text-decoration: none; }
  .alert-menu-item:hover { background: var(--surface); }
  .alert-menu-item.danger { color: var(--red, #e74c3c); }
  .alert-menu-sep { height: 1px; background: var(--border); margin: 4px 5px; }
  /* Groups the 4 report options under the same popover as Archive/View
     listing — consolidated to one flat list, no nested submenu (Luke,
     2026-09-01: "find a way to consolidate things on this menu"). */
  .alert-menu-section { font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); padding: 7px 9px 3px; }
  /* One row of 4 icon buttons instead of 4 stacked full-width rows — the
     popover was taller than the card itself with those stacked (Luke,
     2026-09-01: "fit this within the height of the alert card"). Icon +
     a 2-line label underneath, same compact toolbar shape as e.g. an
     emoji-reaction bar; the label still says what each icon does, it's
     just below instead of beside. */
  /* Fixed-width, evenly spaced rather than flex:1-stretched — Luke,
     2026-09-02: "should look a little more like buttons... but subtle
     and compact and consistent size" — a faint resting border/fill
     reads as a button at rest, not just on hover, while staying quiet
     next to the alert card's own CTAs. */
  .alert-menu-report-row { display: flex; justify-content: space-between; gap: 0px; padding: 3px 1px 3px; }
  .alert-menu-report-btn {
    width: 48px; flex-shrink: 0; display: flex; flex-direction: column; align-items: center; gap: 3px;
    padding: 6px 2px 6px; border: none; background: var(--surface); border-radius: 7px; cursor: pointer;
    color: var(--muted); font-family: inherit; font-size: 9px; line-height: 1.15; text-align: center;
    transition: background .12s, border-color .12s, color .12s;
  }
  .alert-menu-report-btn:hover { background: var(--surface2); border-color: var(--border2); color: var(--text); }
  .alert-menu-report-btn svg { width: 15px; height: 15px; flex-shrink: 0; }

  .alert-body { padding: 10px 14px 14px; display: flex; gap: 12px; align-items: flex-start; position: relative; flex: 1; min-width: 0; }
  .alert-thumb-sm { height: 87px; aspect-ratio: 1/1; object-fit: cover; border-radius: 8px; cursor: zoom-in; flex-shrink: 0; border: 1px solid var(--border2); transition: opacity .15s; }
  .alert-thumb-sm:hover { opacity: .85; }
  .alert-thumb-sm-empty { height: 87px; aspect-ratio: 1/1; flex-shrink: 0; background: var(--surface2); border-radius: 8px; border: 1px solid var(--border); }

  .alert-title { font-size: 14px; font-weight: 500; margin-bottom: 2px; line-height: 1.3; }
  .alert-set { font-size: 12px; color: var(--muted); margin-bottom: 5px; }
  .alert-seller-row { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 5px; flex-wrap: wrap; row-gap: 3px; margin-bottom: 8px; }
  .alert-seller-row .dot { color: var(--dim); }
  .alert-seller-row .star { color: var(--amber); }
  .alert-seller-row .bundle { color: var(--green); }
  .alert-price-row { display: flex; align-items: baseline; gap: 9px; }
  .alert-price-paid { font-size: 18px; font-weight: 600; color: var(--text); }
  .alert-price-mkt { font-size: 12.5px; color: var(--muted); }
  /* Seller quietly raised the price after listing it — see a.first_price
     vs a.price in alertHTML/_update_alert_price (app_server.py). */
  .alert-price-warn { color: var(--amber); flex-shrink: 0; align-self: center; cursor: default; }

  .alert-cta {
    width: calc(100% - 28px); margin: 0 14px 14px; height: 38px; border: none; border-radius: 9px;
    color: #fff; font-size: 13.5px; font-weight: 500; display: flex; align-items: center; justify-content: center;
    gap: 6px; cursor: pointer; text-decoration: none; transition: opacity .15s;
  }
  .alert-cta:hover { opacity: .88; }
  .alert-cta.amber { background: var(--amber-fill); color: var(--amber-contrast); }
  .alert-cta.green { background: var(--green-fill); color: var(--green-contrast); }
  .alert-cta.secondary { background: var(--surface2); color: var(--text); border: 1px solid var(--border2); }
  .alert-cta.win { background: var(--best-bg); color: var(--best); border: 2px solid var(--best); }
  .alert-cta.win.claim-pending { opacity: .55; cursor: default; }
  .alert-cta.win.claim-pending:hover { opacity: .55; }
  .claim-countdown { font-variant-numeric: tabular-nums; }
  /* Claim-flow lock states — checking the listing, confirming price, the
     failure message, and the brief "won" celebration before the card
     leaves the list. See _claimPinned in dashboard.js. */
  .alert-cta.win.checking, .alert-cta.win.checking:hover { opacity: .8; cursor: default; }
  .alert-cta.win.failed, .alert-cta.win.failed:hover {
    background: var(--red-bg); color: var(--red); border-color: var(--red); opacity: 1; cursor: default;
  }
  .alert-cta.win.success, .alert-cta.win.success:hover {
    background: var(--best); color: var(--amber-contrast); opacity: 1; cursor: default; font-weight: 600;
  }
  .cc-spinner {
    width: 14px; height: 14px; flex-shrink: 0; border-radius: 50%;
    border: 2px solid rgba(120,120,120,.3); border-top-color: currentColor;
    animation: cc-spin .7s linear infinite;
  }
  @keyframes cc-spin { to { transform: rotate(360deg); } }
  /* Shown briefly in a pane's content root right when it's entered, if a
     real network fetch (Wallet/Shop) hasn't resolved yet — Luke,
     2026-09-15: "loading wheels if they are needed" for the panes that
     now get torn down and rebuilt on every visit rather than left
     sitting in the DOM (see PANE_TEARDOWN_ROOTS/showPane in
     dashboard.js). A bigger, centered .cc-spinner, not a new spinner. */
  .pane-loading { display: flex; align-items: center; justify-content: center; padding: 60px 0; color: var(--muted); }
  .pane-loading .cc-spinner { width: 22px; height: 22px; border-width: 3px; }
  /* Plays once when a torn-down pane's content is freshly rebuilt on
     re-entry, so content appearing after a (usually near-instant) rebuild
     reads as a deliberate reveal rather than a jarring pop-in. */
  @keyframes pane-content-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
  .pane-fade-in { animation: pane-content-in .18s ease-out; }
  @media (prefers-reduced-motion: reduce) { .pane-fade-in { animation: none; } }
  .alert-cta-row { display: flex; gap: 8px; margin: 0 14px 14px; }
  .alert-cta-row .alert-cta.half { width: auto; flex: 1; margin: 0; }
  .alert-won-lbl { font-size: 12px; color: var(--muted); font-style: italic; margin: 0 14px 14px; }

  .alert-verified { display: flex; align-items: center; gap: 6px; margin: -4px 14px 14px; font-size: 12px; color: var(--muted); }
  /* Scoped to .alert-verified-status-icon specifically, not a bare "svg" —
     that used to also catch the nested Refresh-status button's own icon
     (a descendant regardless of depth), overriding its currentColor
     inheritance from the button's inline color and making icon/text
     mismatch (icon stuck green/muted while the button's text correctly
     followed the alert's buy/negotiate colour). */
  .alert-verified.fresh .alert-verified-status-icon { color: var(--green); flex-shrink: 0; }
  .alert-verified.stale .alert-verified-status-icon { color: var(--muted); flex-shrink: 0; }
  .alert-verified-sub { margin-left: auto; color: var(--dim); }
  .alert-verified-refresh { margin-left: auto; display: inline-flex; align-items: center; gap: 4px; background: none; border: none; color: var(--brand); font-size: 12px; font-weight: 400; cursor: pointer; padding: 0; font-family: inherit; }
  .alert-verified-refresh svg { flex-shrink: 0; }
  .alert-verified-refresh:hover { text-decoration: underline; }
  .alert-verified-refresh:disabled { opacity: .6; cursor: default; text-decoration: none; }
  .alert-verified-spinner { width: 12px; height: 12px; border: 2px solid var(--border2); border-top-color: var(--brand); border-radius: 50%; animation: alertVerifiedSpin .7s linear infinite; flex-shrink: 0; }
  @keyframes alertVerifiedSpin { to { transform: rotate(360deg); } }

  .alert-status-badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }
  .alert-status-badge.gone { background: rgba(231,76,60,0.1); color: rgba(231,76,60,0.85); border: 1px solid rgba(231,76,60,0.25); }
  .alert-status-badge.archived { background: var(--surface2); color: var(--muted); border: 1px solid var(--border2); }

  .alert-gone-banner { font-size: 13px; color: var(--text-secondary); background: var(--brand-bg); border: 1px solid var(--brand-border); border-radius: var(--radius); padding: 10px 14px; margin-bottom: 12px; }
  .alert-gone-banner b { color: var(--text); }

  .alert-filter-bar { display: flex; gap: 6px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
  /* Lost/Archived pushed to the right edge — Luke, 2026-09-15: "I also
     wan[t] the Lost and Archived pills to range to the right, leaving
     the others on the left" (this used to only apply on desktop; mobile
     flowed .alert-filter-right's chips inline with everything else,
     which just left them wrapping wherever they happened to land). As a
     single flex child of .alert-filter-bar, when this group doesn't fit
     on the first line it wraps as one unit onto its own line, where
     margin-left:auto then pushes it to that line's right edge — exactly
     the two-row layout in Luke's screenshot annotation. */
  .alert-filter-right { margin-left: auto; display: flex; gap: 6px; align-items: center; }
  .alert-filter-chip { display: flex; align-items: center; justify-content: center; gap: 4px; font-size: 11px; padding: 4px 11px; border-radius: 14px; border: 1px solid var(--border2); background: var(--surface2); color: var(--muted); cursor: pointer; font-family: inherit; transition: all .15s; white-space: nowrap; }
  .alert-filter-chip.on.all { background: var(--text); border-color: var(--muted); color: var(--bg); font-weight: 500; }
  .alert-filter-chip.on.buy { background: rgba(46,204,113,0.12); border-color: rgba(46,204,113,0.35); color: var(--green); }
  .alert-filter-chip.on.negotiate { background: var(--amber-bg); border-color: var(--amber-border); color: var(--amber); }
  .alert-filter-chip.on.gone { background: rgba(231,76,60,0.12); border-color: rgba(231,76,60,0.35); color: rgba(231,76,60,0.9); }
  .alert-filter-chip.on.muted { background: var(--brand-bg); border-color: var(--brand-border); color: var(--brand); }
  .afc-count { opacity: .65; font-size: 11px; }
  /* Mobile: count drops below the label instead of crowding the same
     line — Luke, 2026-09-02: "if we are in mobile view, I want the
     filter number to appear below the filter text. But it can be on
     the same row in desktop view." Every chip always renders a count
     slot now (dashboard.js's cnt() falls back to "-"), so a chip with
     no alerts still reserves its second line — Luke, 2026-09-15: "I
     want all of these filter pills to be the same height even if they
     don't have an alert count." A taller, two-line chip also reads
     better with a bit more corner rounding than the single-line pill
     shape (14px) needs.  */
  @media (max-width: 800px) {
    .alert-filter-chip { flex-direction: column; gap: 0; line-height: 1.3; padding: 4px 10px; border-radius: 18px; }
  }
  .metric.buy-metric { border-color: rgba(46,204,113,0.22); }
  .metric.neg-metric { border-color: rgba(240,165,0,0.22); }
  .act-btn {
    font-size: 11px; padding: 5px 12px; border-radius: var(--radius);
    cursor: pointer; font-family: inherit; text-decoration: none;
    display: inline-block; transition: all .15s; border: 1px solid;
  }
  .act-btn.primary { background: var(--brand); color: var(--brand-contrast); border-color: var(--brand); font-weight: 500; }
  .act-btn.primary:hover { background: var(--brand-hover); border-color: var(--brand-hover); }
  .act-btn.primary.amber { background: var(--amber-fill); color: var(--amber-contrast); border-color: var(--amber); }
  .act-btn.secondary { background: transparent; color: var(--muted); border-color: var(--dim); }
  .act-btn.secondary:hover { color: var(--text); border-color: var(--border2); }

  .alert-ts { font-size: 11px; color: var(--muted); white-space: nowrap; padding-top: 2px; }

  .empty-state { text-align: center; padding: 60px 20px; color: var(--muted); }
  .empty-icon { font-size: 32px; margin-bottom: 10px; opacity: .4; }
  .empty-text { font-size: 13px; }

  /* ── Dashboard: stat tiles, upgrade banner, market data, spotlight ── */
  /* border-radius here matters beyond its own look — the tutorial's
     border-beam highlight (dashboard.css, .tutorial-highlight::before)
     reads it via border-radius: inherit, and a container with none of
     its own made a beam wrapping it look square even though every
     card inside is rounded (Tom, 2026-09-18). */
  .dash-tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 14px; border-radius: var(--radius-lg); }
  .dash-tile { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px 16px; }
  .dash-tile-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; margin-bottom: 6px; }
  .dash-tile-val { font-size: 22px; font-weight: 600; color: var(--text); line-height: 1; }
  .dash-tile-val.green { color: var(--green); }
  @media (max-width: 800px) { .dash-tiles { grid-template-columns: repeat(2, 1fr); } }

  .dash-upgrade { display: flex; align-items: center; gap: 14px; border: 1.5px dashed var(--brand-border); background: var(--brand-bg); border-radius: var(--radius-lg); padding: 14px 16px; margin-bottom: 14px; }
  .dash-upgrade-icon { width: 40px; height: 40px; border-radius: 10px; background: rgba(232,37,26,.12); display: flex; align-items: center; justify-content: center; color: var(--brand); flex-shrink: 0; }
  .dash-upgrade-title { color: var(--text); }
  .dash-upgrade-sub { font-size: 11px; color: var(--muted); margin-top: 2px; }
  .dash-upgrade.invite { border-color: var(--boost-border); background: var(--boost-bg); }
  .dash-upgrade.invite .dash-upgrade-icon { background: var(--boost-bg); color: var(--boost); }
  .act-btn.primary.invite { background: var(--boost); border-color: var(--boost); }
  .act-btn.primary.invite:hover { background: var(--boost-deep); border-color: var(--boost-deep); }

  /* Animated "border beam" glow — Tom, 2026-09-16, referencing 21st.dev's
     Button Border component: a bright arc of the brand gradient
     continuously sweeping around a card's own edge, layered on top of
     its existing border rather than replacing it. CSS-only (a spinning
     conic-gradient masked down to just the border ring via the
     content-box/exclude trick) — no JS/animation library needed. Used
     on the Membership upgrade callout and "Chase your first card". The
     animated custom property degrades gracefully to a static (non-
     spinning) ring on browsers without @property support, never to
     nothing. */
  @property --border-beam-angle {
    syntax: '<angle>';
    inherits: false;
    initial-value: 0deg;
  }
  .glow-border { position: relative; z-index: 0; }
  .glow-border::before {
    content: ""; position: absolute; inset: -1px; z-index: -1;
    border-radius: inherit; padding: 1.5px;
    background: conic-gradient(from var(--border-beam-angle), transparent 0deg, transparent 270deg, var(--brand) 315deg, var(--brand-hover) 337deg, transparent 360deg);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    mask-composite: exclude;
    animation: border-beam-spin 3.5s linear infinite;
    pointer-events: none;
  }
  @keyframes border-beam-spin { to { --border-beam-angle: 360deg; } }
  @media (prefers-reduced-motion: reduce) {
    .glow-border::before { animation: none; }
  }
  /* Tom, 2026-09-18: "Now let's watch a card" targets "Chase your first
     card", which already carries this exact beam permanently — rather
     than stacking a second tutorial-highlight ring on top, this recolors
     the existing beam purple for the step's duration (see
     nativeGlowBorder in dashboard.js). Padding (ring thickness) bumped
     50% over the base 1.5px too — Tom: "make the chase your first card
     purple animation 50% thicker so it's really noticeable." */
  .glow-border.tutorial-purple::before {
    background: conic-gradient(from var(--border-beam-angle), transparent 0deg, transparent 270deg, var(--tutorial-accent) 315deg, var(--tutorial-accent) 337deg, transparent 360deg);
    padding: 2.25px;
  }
  /* Tom, 2026-09-18: "for duration of the tutorial do not show the red
     animation around chase your first card" — its permanent .glow-border
     reads as an ordinary red highlight everywhere else in the app, but
     during the tour every OTHER highlight is purple, so the one card
     still spinning red (whenever it isn't the current step's own
     purple target) read as a mismatched leftover. Suppressed for the
     whole tour via this body class (set in startTutorial/
     _tutorialFinish); :not(.tutorial-purple) leaves the "Now let's
     watch a card" step's own purple recolor (above) untouched. */
  body.tutorial-active #dash-first-card .glow-border:not(.tutorial-purple)::before {
    animation: none;
    background: none;
  }

  /* Daily Streak card — the runner strip and its icon states are all
     theme-aware (var(--bg)/var(--dim)/var(--text)) rather than the fixed
     dark-panel + white-icon look this started as — the fixed white fills
     had ~0 contrast once the strip itself stopped being hardcoded near-
     black. .revealed's true colors (the card's own red/orange) stay fixed
     across themes on purpose — that's the "real" prize color, not a UI
     state color. */
  .streak-card { position: relative; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); margin-bottom: 15px; padding: 16px; }
  /* border-radius here matters beyond its own look, same reasoning as
     .dash-tiles above — the tutorial highlight targets this empty
     wrapper (its own innerHTML is populated later), which otherwise
     has no radius of its own for that beam to inherit. */
  #streak-section { border-radius: var(--radius-lg); }
  /* Same reasoning again (Tom, 2026-09-18: "make sure Chase your first
     card also does the purple animation on this step") — the tutorial
     highlight here targets the OUTER #dash-first-card wrapper, not its
     inner .glow-border child, specifically so it gets its own plain
     .tutorial-highlight ring instead of colliding with the
     body.tutorial-active suppression rule that strips the red beam off
     every .glow-border element during the tour. The outer wrapper has
     no border-radius of its own for that ring to inherit. */
  #dash-first-card { border-radius: var(--radius-lg); }
  .streak-title { color: var(--text); padding-right: 26px; }
  .streak-desc { font-size: 11px; color: var(--muted); margin-top: 2px; margin-bottom: 14px; line-height: 1.4; }
  .streak-strip { position: relative; background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 16px 6px 11px 6px; display: flex; align-items: center; margin-bottom: 12px; overflow: visible; }
  /* Cumulative-progress underline (Luke, 2026-09-12) — fills left-to-right
     under the row, one cell-width per completed day. Grows live during a
     claim (see claimStreak()'s direct width write on #streak-progress-fill)
     rather than only ever snapping to its final value on re-render, so it
     visibly advances alongside the flashing icon instead of jump-cutting. */
  .streak-progress-track { position: absolute; left: 0; right: 0; bottom: 0; height: 4px; border-radius: 0 0 10px 10px; background: var(--border2); overflow: hidden; z-index: 0; }
  .streak-progress-fill { height: 100%; background: var(--brand); transition: width 1.1s cubic-bezier(.4,0,.2,1); }
  .streak-cell { position: relative; z-index: 1; flex: 1; display: flex; align-items: center; justify-content: center; }
  .streak-cell svg { height: 22px; width: auto; display: block; }
  /* Not-yet-earned days (.today included — nothing's claimed until it's
     actually claimed, so it shouldn't look "achieved" ahead of time) render
     the same as .future. Only .done is white, and only .latest (the one
     specific cell claimed *today*, not every past .done day) pulses. */
  .streak-cell.done svg { fill: var(--text); }
  .streak-cell.latest svg { filter: drop-shadow(0 0 6px color-mix(in srgb, var(--text) 55%, transparent)); animation: streak-pulse 1.3s ease-in-out infinite; }
  .streak-cell.today svg, .streak-cell.future svg { fill: var(--dim); }
  @keyframes streak-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.18); } }
  /* Claim in flight (Luke, 2026-09-12: the claim can have a real DB-check
     delay with nothing visible happening) — the about-to-be-claimed cell
     flashes dim/solid immediately on click, a fixed 3 cycles (see
     MIN_FLASH_MS in claimStreak()), regardless of how fast the server
     actually answers, so the "checking" moment always reads the same. */
  .streak-cell.flashing svg { animation: streak-flash .4s ease-in-out 3; }
  @keyframes streak-flash { 0%, 100% { fill: var(--dim); } 50% { fill: var(--text); } }
  /* Day 10: the runner sits in its natural position (same as every other
     cell, unshifted) — the card sits behind it (lower z-index), offset so
     only 30% of the card's own width peeks out past the runner. */
  .streak-finale { position: relative; display: flex; align-items: center; }
  .streak-finale > svg { height: 22px; width: auto; display: block; position: relative; z-index: 1; }
  .streak-finale.future > svg, .streak-finale.today > svg { fill: var(--dim); }
  .streak-finale.done > svg { fill: var(--text); }
  .streak-finale.latest > svg { filter: drop-shadow(0 0 6px color-mix(in srgb, var(--text) 55%, transparent)); animation: streak-pulse 1.3s ease-in-out infinite; }
  /* The card (the prize) is knocked back white/dim — same states as the 10th
     runner (.p1/.p2 classes replace the asset's own baked-in fill so CSS can
     drive it) — until day 10 is actually reached AND claimed. True two-tone
     color + orange glow only show in .revealed, applied for one beat right
     at the claim itself, not just from showing "today".
     Height is derived from the runner's own scale (22px / runner viewBox
     height 26.06) applied to the card's viewBox height (40.52), not picked
     independently — the two assets' viewBoxes share a design scale
     (40.52/26.06 ≈ 1.555x), so sizing them separately understated the card.
     left/top are Luke's own hand-tuned values for the real assets — don't
     "fix" them without checking with him first. */
  .streak-finale-card { position: absolute; left: 16.4px; top: -9px; z-index: 0; }
  .streak-finale-card svg { height: 34.21px; width: auto; display: block; fill: var(--dim); }
  .streak-finale.done .streak-finale-card svg { fill: var(--text); }
  /* Card gets its own pulse (not the runner's streak-pulse) because it needs
     a small rightward translateX at peak scale — measured directly: the
     runner (wider, 27px) grows ~2.46px on its right edge at scale(1.18) vs
     the card (narrower, 15px) growing only ~1.29px, since both scale around
     their own center and the wider one moves more in absolute px. Without
     compensation the two visibly drift apart mid-pulse. ~1.2px covers the
     measured gap — nudge further if it still looks off. Only .revealed
     pulses — that only ever applies the day it's actually claimed. */
  .streak-finale.revealed .streak-finale-card svg { filter: drop-shadow(0 0 7px rgba(255,107,61,.75)); animation: streak-pulse-card 1.3s ease-in-out infinite; }
  @keyframes streak-pulse-card { 0%, 100% { transform: scale(1) translateX(0); } 50% { transform: scale(1.18) translateX(1.2px); } }
  .streak-finale.revealed .streak-finale-card svg .p1 { fill: #e3272e; }
  .streak-finale.revealed .streak-finale-card svg .p2 { fill: #f04f23; }
  /* min-height pins the claim-button row and the post-claim "Claimed for
     today" row to the same height (27px, the taller of the two — the
     button's own padding beats the badge, but the plain claimed-text row
     is shorter than the badge) so swapping between them doesn't nudge the
     card's total height by a couple px. Doing it here (not a fixed height
     on .streak-card itself) keeps the rest of the card — title/desc/strip —
     free to grow naturally, e.g. when the subtitle wraps to 2 lines on
     narrow mobile widths. */
  .streak-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; min-height: 27px; }
  .streak-badge { font-size: 12px; font-weight: 600; color: var(--text); background: var(--surface2); border: 1px solid var(--border2); padding: 4px 10px; border-radius: var(--r-pill); white-space: nowrap; }
  .streak-badge b { color: var(--brand); }
  .streak-card.claiming { pointer-events: none; }
  .streak-card.leaving { animation: streak-leave .5s ease forwards; }
  @keyframes streak-leave { to { opacity: 0; transform: scale(.97) translateY(-6px); margin-bottom: -14px; padding-top: 0; padding-bottom: 0; max-height: 0; overflow: hidden; } }
  /* Claimed-today resting state — stays put until the user dismisses it
     themselves (.streak-close), rather than auto-hiding after the claim.
     Close sits top-right of the whole card (not inline in .streak-foot) —
     .streak-card has position:relative and .streak-title gets right
     padding so its text doesn't run underneath the button. */
  .streak-claimed-msg { font-size: 12px; font-weight: 600; color: var(--green); display: flex; align-items: center; gap: 5px; }
  .streak-close { position: absolute; top: 10px; right: 10px; width: 26px; height: 26px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border: none; background: none; color: var(--muted); cursor: pointer; border-radius: var(--radius); }
  .streak-close:hover { color: var(--text); background: var(--surface2); }

  /* 50% width per card, but never narrower than 370px — collapses to a
     single column rather than squeezing cards under that floor. Same
     max(floor, 50%-halfgap) idiom as .mob-watched-grid. */
  .mkt-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(370px, 100%), 1fr)); gap: 10px; margin-bottom: 14px; }
  .mkt-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px 16px; }
  .mkt-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
  .mkt-card-titlewrap { min-width: 0; }
  .mkt-card-title { font-size: 13px; font-weight: 600; color: var(--text); }
  .mkt-card-desc { font-size: 11px; color: var(--muted); margin-top: 2px; line-height: 1.35; }
  .mkt-card-sub { font-size: 11px; color: var(--muted); display: flex; align-items: center; gap: 4px; flex-shrink: 0; white-space: nowrap; }
  .mkt-card-sub .market-price-icon { width: 11px; height: 11px; flex: 0 0 11px; }
  .mkt-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-top: 1px solid var(--border2); }
  .mkt-row:first-of-type { border-top: none; }
  .mkt-rank { font-size: 13px; font-weight: 700; color: var(--muted); width: 14px; flex-shrink: 0; text-align: center; }
  /* Thumbnail is cropped to a square (top of the card art only, via
     object-position:top below) and bottom-aligned with margin-bottom
     cancelling the row's own padding-bottom, so its bottom edge lands
     exactly on the row's border-box edge — the same line the next row's
     divider sits on — making it read as popping up in front of the divider.
     Bottom corners are square (not rounded) since that edge sits flush
     against the divider line, not floating. */
  .mkt-art { height: 70px; aspect-ratio: 1/1; border-radius: 5px 5px 0 0; border: 1px solid var(--border); background: var(--surface2); overflow: hidden; flex-shrink: 0; align-self: flex-end; margin-bottom: -7px; }
  .mkt-art img { width: 100%; height: 100%; object-fit: cover; object-position: top; }
  /* Hover-eye "watch this card" affordance, Weekly Best Sellers only (Luke,
     2026-10-15: "do the same for the dashboard version"). Same shape as
     home.html's .seller-thumb-wrap/.seller-eye. */
  .mkt-art.mkt-art-watchable { position: relative; cursor: pointer; }
  .mkt-art-eye {
    position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
    background: rgba(0,0,0,.6); color: #fff; opacity: 0; transition: opacity .15s;
  }
  .mkt-art-watchable:hover .mkt-art-eye { opacity: 1; }
  .mkt-art-eye svg { width: 22px; height: 22px; }
  .mkt-info { flex: 1; min-width: 0; }
  .mkt-name { font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .mkt-sub { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .mkt-stat { text-align: right; flex-shrink: 0; }
  .mkt-price { font-size: 12px; font-weight: 600; color: var(--text); }
  .mkt-count { font-size: 12px; color: var(--muted); margin-top: 1px; }
  .mkt-pct { font-size: 11px; font-weight: 600; color: var(--green); display: flex; align-items: center; gap: 2px; justify-content: flex-end; margin-top: 1px; }

  /* Watched cards summary — mobile only (see @media(max-width:800px) below);
     the sidebar's own watch list becomes an off-canvas drawer at that width,
     so this surfaces the same "add + top watches" pattern inline instead. */
  .mob-watched-section { display: none; margin-bottom: 14px; }
  /* At most 2 columns, never narrower than 205px per card (matches the
     desktop sidebar's own .wcm width). Each column's minimum is whichever
     is larger of 205px or half the row (minus half the gap) — so it holds
     at 2 columns on wide containers, but collapses to 1 column rather than
     squeezing cards under the 205px floor on narrow ones. */
  .mob-watched-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(max(205px, calc(50% - 5px)), 1fr)); gap: 10px; }
  .mob-watched-grid .wcm { margin-bottom: 0; }
  .mwc-see-more { grid-column: 1 / -1; text-align: center; font-size: 12px; font-weight: 600; color: var(--brand); padding: 12px; border: 1px dashed var(--border2); border-radius: var(--radius-lg); cursor: pointer; background: var(--surface2); }
  .mwc-see-more:hover { border-color: var(--brand-border); }

  /* Deal of the Week — content layout adopted from the home page's
     .deal-* / .highlight-* pattern (static/home.css ~line 614), minus
     the header icon (deliberately excluded here). Header row holds the
     page_section_title label + a save-amount badge, same split as
     home's .highlight-header.has-badge; body is thumbnail + info column
     with a bold price and two label+strong lines underneath; a meta row
     below carries the "Won by" roundel — same avatar methodology
     (colored circle, initial letter) as home's .deal-meta-avatar,
     generalised as .username-avatar for reuse anywhere a ChaseCard
     username needs a profile picture. */
  .spotlight-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
  .spotlight-save-pill { font-size: 12px; font-weight: 700; color: var(--green); background: var(--green-bg); padding: 4px 11px; border-radius: var(--r-pill); flex-shrink: 0; }
  .spotlight-card { display: flex; flex-direction: column; gap: 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px 16px; margin-bottom: 14px; }
  .spotlight-body { display: flex; gap: 16px; }
  .spotlight-art { height: 120px; aspect-ratio: 5/7; border-radius: 8px; border: 1px solid var(--border); background: var(--surface2); overflow: hidden; flex-shrink: 0; box-shadow: 0 6px 16px rgba(0,0,0,.15); }
  .spotlight-art img { width: 100%; height: 100%; object-fit: cover; object-position: top; }
  .spotlight-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; justify-content: center; }
  .spotlight-price { margin-top: 2px; }
  .spotlight-market { font-size: 12px; font-weight: 400; color: var(--muted); }
  .spotlight-market strong { font-size: 16px; font-weight: 700; color: var(--text); }
  .spotlight-save { font-size: 12px; font-weight: 600; color: var(--green); }
  .spotlight-save strong { font-size: 16px; font-weight: 800; }
  .spotlight-meta { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
  .spotlight-meta strong { color: var(--text); }
  .username-avatar { width: 24px; height: 24px; border-radius: 50%; background: linear-gradient(135deg, var(--brand), #f5b400); display: flex; align-items: center; justify-content: center; font-family: var(--font); font-weight: 700; font-size: 11px; color: #fff; flex-shrink: 0; }

  /* ── Add Watch pane ── */
  .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; max-width: 560px; }
  .form-grid.full { grid-template-columns: 1fr; }
  /* Card name/Number row — a nested grid inside .form-grid, not .form-grid
     itself, so it needs its own copy of the same 1fr collapse below (it
     used to be an inline style, which no media query could ever reach —
     that's why it kept overflowing the card on mobile no matter how
     narrow .form-grid's own columns got). */
  .aw-name-num-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  /* Affiliate application form (Tom, 2026-09-24: "user details on the
     left, socials on the right") — the pitch textarea and submit button
     stay outside this grid, full-width below both columns. */
  .aa-form-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0 20px; }
  @media (max-width: 500px) { .aa-form-cols { grid-template-columns: 1fr; } }
  .field { display: flex; flex-direction: column; gap: 5px; }
  .field label { font-size: 11px; color: var(--muted); }
  .field input, .field select {
    background: var(--surface2); border: 1px solid var(--border2);
    border-radius: var(--radius); padding: 8px 10px;
    color: var(--text); font-family: var(--font); font-size: 13px;
    outline: none; transition: border-color .15s;
  }
  #ref-apply-code-input { background: var(--surface2); border: 1px solid var(--border2); border-radius: var(--radius); padding: 9px 12px; color: var(--text); font-family: var(--font); font-size: 13px; outline: none; transition: border-color .15s; }
  #ref-apply-code-input:focus { border-color: var(--brand); }
  #ref-apply-code-input::placeholder { color: var(--dim); text-transform: none; letter-spacing: 0; font-weight: 400; }
  .field input:focus, .field select:focus { border-color: var(--brand); }
  .field input::placeholder { color: var(--dim); }
  .field select option { background: var(--surface2); }
  /* UK County's native arrow sat too close to the box's own right edge
     (Luke, 2026-09-24: "needs to move 6px left") — the OS-drawn arrow
     can't be nudged directly, so this swaps it for a custom chevron
     (same stroke/shape as the app's other dropdown chevrons) positioned
     exactly where asked. */
  #s-uk-county {
    appearance: none; -webkit-appearance: none; -moz-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238A909A' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 14px center;
    background-size: 14px;
    padding-right: 34px;
  }
  .field input:disabled { opacity: .6; cursor: not-allowed; }

  /* Unified Alerts slider (Search mode) — one track, three handles
     (Reject/Buy/Offer), replacing the old three-separate-sliders layout.
     Built from the reviewed Design Artifact mockup; see dashboard.html's
     own comment on #aw-unified-slider and awUsStartDrag/awUsToggle/
     awUsPaint in dashboard.js for the drag + zone-coloring logic.
     --aw-offer is the mockup's own orange (Luke, 2026-10-10: "those are
     the orange colours to use on the slider", pointing at the mockup,
     not the site's existing --amber token — #D97706/£F6B562 reads too
     brown next to the mockup's #F59E0B). Scoped to this component only. */
  .aw-unified-slider { --aw-offer: #F59E0B; --aw-offer-bg: rgba(245,158,11,.1); --aw-offer-border: rgba(245,158,11,.3); }
  .aw-us-title { margin-bottom: 14px; }
  .aw-us-labels { position: relative; height: 20px; margin-bottom: 8px; }
  .aw-us-label { position: absolute; top: 0; transform: translateX(-50%); font-size: 12px; font-weight: 700; white-space: nowrap; }
  /* Reject uses --red throughout (label, zone, handle, toggle, card),
     not grey — Luke, 2026-10-10, annotated screenshot: "RED" with arrows
     at the Reject toggle and track zone. Matches the Watch Detail page's
     own Reject editor, which has always used red for this (see
     .rej-head's settings-toggle override elsewhere in this file) — the
     mockup's own grey was the odd one out, not the site convention. */
  .aw-us-label-reject { color: var(--red); }
  .aw-us-label-buy { color: var(--green); }
  .aw-us-label-offer { color: var(--aw-offer); }
  .aw-us-track { position: relative; height: 10px; border-radius: 999px; background: var(--border2); margin: 0 11px; }
  /* Clips the zones to the track's own rounded-pill shape at both ends —
     a square-cornered zone (buy/offer/none) landing right at an edge used
     to poke a square corner out past the track's rounded end (e.g. green
     starting at 0% when Reject is off). Handles/market live outside this
     layer (siblings, not children) since they're taller than the track
     and would get clipped too. Luke, 2026-10-10: "can we have a rounded
     edge on the left of the slider bar? at the 90% below point?" */
  .aw-us-track-fill { position: absolute; inset: 0; border-radius: 999px; overflow: hidden; }
  .aw-us-zone { position: absolute; top: 0; bottom: 0; }
  .aw-us-zone-reject { border-radius: 999px 0 0 999px; background: var(--red); }
  .aw-us-zone-buy { background: var(--green); }
  .aw-us-zone-offer { background: var(--aw-offer); }
  .aw-us-zone-none { border-radius: 0 999px 999px 0; background: var(--border2); }
  .aw-us-market { position: absolute; top: -9px; bottom: -9px; width: 2px; background: var(--text); opacity: .5; }
  .aw-us-handle { position: absolute; top: 50%; width: 20px; height: 20px; border-radius: 50%; background: var(--surface); cursor: grab; touch-action: none; box-shadow: 0 1px 3px rgba(0,0,0,.35); transform: translate(-50%, -50%); }
  .aw-us-handle:active { cursor: grabbing; }
  .aw-us-handle-reject { border: 3px solid var(--red); }
  .aw-us-handle-buy { border: 3px solid var(--green); width: 22px; height: 22px; z-index: 2; }
  .aw-us-handle-offer { border: 3px solid var(--aw-offer); }
  .aw-us-caption { display: flex; justify-content: space-between; margin: 10px 11px 0; font-size: 10.5px; color: var(--muted); }

  /* The three mini-cards below the track — styled to match the mockup
     screenshot directly (Luke, 2026-10-10): Reject/Buy/Offer order, bold
     neutral % value with a muted "below"/"above" unit, Buy filled green
     with an "Always on" pill, Reject/Offer plain cards with a toggle. */
  .aw-us-cards { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 10px; margin-top: 22px; }
  .aw-us-card { min-width: 0; background: var(--surface); border: 1px solid var(--border2); border-radius: var(--radius-lg, 14px); padding: 14px 12px; display: flex; flex-direction: column; gap: 8px; }
  .aw-us-card-reject { border-color: var(--red-bg); }
  .aw-us-card-buy { background: var(--green-bg); border-color: var(--green-border); }
  .aw-us-card-offer { border-color: var(--aw-offer-border); }
  .aw-us-card-head { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
  .aw-us-card-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .02em; white-space: nowrap; }
  .aw-us-card-label-reject { color: var(--red); }
  .aw-us-card-label-buy { color: var(--green); }
  .aw-us-card-label-offer { color: var(--aw-offer); }
  /* Reject's toggle is red whether on or off (matching .rej-head's
     existing sitewide always-red behavior), not the generic green-on
     every other settings-toggle uses. */
  .aw-us-card-toggle-reject.settings-toggle .settings-toggle-track { background: var(--red-bg); }
  .aw-us-card-toggle-reject.settings-toggle input:checked + .settings-toggle-track { background: var(--red); }
  .aw-us-card-val { font-size: 18px; font-weight: 700; color: var(--text); }
  .aw-us-card-unit { font-size: 12px; font-weight: 500; color: var(--muted); }
  .aw-us-card-sub { font-size: 11px; color: var(--muted); line-height: 1.4; }
  .aw-us-card-toggle.settings-toggle { width: 30px; height: 17px; flex-shrink: 0; }
  .aw-us-card-toggle.settings-toggle .settings-toggle-track::before { width: 11px; height: 11px; }
  .aw-us-card-toggle.settings-toggle input:checked + .settings-toggle-track::before { transform: translateX(13px); }
  .aw-us-card-toggle-offer.settings-toggle input:checked + .settings-toggle-track { background: var(--aw-offer); }
  /* Luke, 2026-10-25: "the offer toggle should be 'offer orange'" — Manual
     mode's own Offer alert toggle; var(--amber) rather than
     var(--aw-offer) (Search mode's unified-slider-scoped orange token,
     not defined outside .aw-unified-slider) since this toggle lives
     outside that component — matches the "Offer alert" label right above
     it, which already uses var(--amber). */
  .aw-manual-offer-toggle.settings-toggle input:checked + .settings-toggle-track { background: var(--amber); }
  /* Class-based (not #reject-box's old id-only form) so the same rule
     greys out both Add Watch's bare card and Watch Detail's per-watch
     card (id suffixed -${wid}) — Luke, 2026-10-14: "I can't say once
     I've disabled the Offer Threshold... and it doesn't grey out". */
  .aw-us-card.reject-off, .aw-us-card.offer-off { opacity: .6; }

  /* Always Alert (Elite+) - the row above the price thresholds, and the "overridden" look of everything under it while on. */
  .aw-aa { display:flex; align-items:center; gap:12px; margin:10px 0 14px; padding:12px 14px; border:1px solid var(--border2); border-radius:var(--radius); background:var(--surface2); transition:border-color .15s, background .15s; }
  .aw-aa-ic { width:30px; height:30px; flex-shrink:0; display:flex; align-items:center; justify-content:center; border-radius:8px; background:rgba(168,85,247,.14); color:#a855f7; }
  .aw-aa-txt { flex:1; min-width:0; }
  .aw-aa-name { font-size:13.5px; font-weight:700; color:var(--text); display:flex; align-items:center; gap:7px; }
  .aw-aa-badge { font-size:9.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#a855f7; }
  .aw-aa-sub { font-size:11.5px; color:var(--muted); margin-top:1px; }
  .aw-aa.on { border-color:#a855f7; background:color-mix(in srgb, #a855f7 12%, var(--surface2)); }
  .aw-aa.on .aw-aa-sub { color:var(--text); }
  .aw-aa-toggle.settings-toggle input:checked + .settings-toggle-track { background:#a855f7; }
  .aw-aa[hidden], .aw-us-subhead[hidden] { display:none; }
  .aw-us-subhead { display:flex; align-items:center; justify-content:space-between; gap:8px; margin:2px 2px 4px; font-size:12.5px; font-weight:600; color:var(--text-secondary, var(--muted)); }
  .aw-us-over { display:inline-flex; align-items:center; gap:5px; font-size:11px; font-weight:600; color:var(--muted); }
  .aw-us-over[hidden] { display:none; }
  .aw-unified-slider.always-on .aw-us-body { opacity:.42; pointer-events:none; user-select:none; filter:grayscale(.6); }
  .aw-aa.wd-aa { margin:0 0 12px; }
  .wd-thr-paused { opacity:.42; pointer-events:none; user-select:none; filter:grayscale(.6); }
  .aw-aa-note { display:flex; align-items:center; gap:6px; margin-top:12px; font-size:11.5px; color:var(--muted); }
  .aw-aa-note[hidden] { display:none; }

  .threshold-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 4px; }
  /* Reject threshold joins Buy/Negotiate below (added 2026-09-11) — a
     third column made all three sliders too cramped to actually use
     (Luke, live: "three side by side is too small... only 1 or 2
     sliders side by side"). Stays in the same 2-column .threshold-row
     grid as Buy/Negotiate; .thresh-box-full spans both columns so it
     gets a full-width row of its own underneath instead of squeezing
     into a half-width slot. */
  .thresh-box-full { grid-column: 1 / -1; }
  /* The "or manually set £" number inputs were clipping their own last
     digit (e.g. "117.07" rendering as "117.0") in some window widths —
     Chrome's native spin-button arrows eat into the box's usable text
     width with no overflow indicator, so a value that's one character too
     wide for what's left just gets cut. Dropping the spinner arrows
     reclaims that space; nothing here used them (typed/synced values
     only, no up/down stepping). */
  #buy-price-input, #neg-price-input, #f-manual-buy-price, #f-manual-neg-price { -moz-appearance: textfield; }
  #buy-price-input::-webkit-outer-spin-button, #buy-price-input::-webkit-inner-spin-button,
  #neg-price-input::-webkit-outer-spin-button, #neg-price-input::-webkit-inner-spin-button,
  #f-manual-buy-price::-webkit-outer-spin-button, #f-manual-buy-price::-webkit-inner-spin-button,
  #f-manual-neg-price::-webkit-outer-spin-button, #f-manual-neg-price::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
  /* Each .thresh-box is its own .wd-card now (not nested inside one shared
     card) — .thresh-box itself carries no box styling of its own anymore. */
  .thresh-box-label { font-size: 11px; color: var(--muted); margin-bottom: 8px; text-transform: uppercase; }
  /* Same disp treatment as Watch Detail's .wd-thr-slider .disp — scoped to
     .thresh-box since Add Watch's sliders sit directly in the box, not
     inside a .wd-thr-slider wrapper (that wrapper's own padding/margin
     were tuned for its own container, not this one). */
  .thresh-box .disp { font-size: 13px; font-weight: 600; margin-bottom: 6px; }
  .thresh-box .disp.buy { color: var(--green); }
  .thresh-box .disp.neg { color: var(--amber); }
  .thresh-box .disp.reject { color: var(--brand); }
  .thresh-box .rej-sub { font-size: 11px; color: var(--muted); margin-top: 10px; line-height: 1.5; }
  .rej-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
  /* Toggled off: the % and slider fade back, matching the greyed style
     .settings-toggle-track already uses in its own unchecked state,
     rather than hiding the slider outright — the value is still there
     to look at, just visibly inert. */
  .thresh-box.reject-off .disp.reject { color: var(--muted); }
  .thresh-box.reject-off input[type=range] { opacity: .35; pointer-events: none; }
  .thresh-box.reject-off .rej-sub { opacity: .6; }
  /* Reject's own toggle stays deep red in both states (Luke, 2026-09-11)
     — unlike every other .settings-toggle on the site (grey off / green
     on), this one isn't "off = neutral, on = good": rejecting listings
     is itself a red/caution action, so on and off both read as red, only
     the knob position changes. */
  .rej-head .settings-toggle .settings-toggle-track { background: var(--red); }
  .rej-head .settings-toggle input:checked + .settings-toggle-track { background: var(--red); }
  /* Offer's toggle on the unified Alerts slider shares .rej-head's layout
     (label + switch, space-between) but not its always-red color — Luke,
     2026-10-10: "get the Make Offer orange correct!" — amber on, not red,
     matching Offer's own theme everywhere else on this card. */
  .rej-head.offer-head .settings-toggle .settings-toggle-track { background: var(--border2); }
  .rej-head.offer-head .settings-toggle input:checked + .settings-toggle-track { background: var(--amber); }

  .form-actions { display: flex; gap: 8px; margin-top: 20px; }
  /* Luke, 2026-10-24: cog/market quick-nav buttons beside the mobile
     sticky "Start watching" bar — hidden outside it (desktop's own
     .form-actions never shows these; see dashboard.html's own comment
     on why). "Square off the buttons a little with rounded edges" — a
     real square, not .aw-start-btn's own full pill radius. */
  .aw-quicknav-btn {
    display: none; flex: 0 0 auto; align-items: center; justify-content: center;
    width: 38px; height: 38px; border-radius: 10px;
    background: var(--surface2); border: 1px solid var(--border2); color: var(--text);
    cursor: pointer; transition: border-color .15s, opacity .15s;
  }
  .aw-quicknav-btn:hover { border-color: var(--muted); }
  .aw-quicknav-btn:active { opacity: .8; }
  /* Luke, 2026-10-09: "on desktop there are some issues with the start
     button cramping up with other elements" — #aw-duration-info (the
     "Free plan: X/Y watches remaining" line) sat flush against the
     button with 0px gap on desktop. .aw-sticky-cta only gets `gap: 8px`
     inside the mobile-only media query further down (it's a plain
     display:block div outside that breakpoint), so desktop never got any
     spacing between the two. Reset to 0 inside that same media query so
     mobile doesn't end up with 8px (flex gap) + 10px (this) stacked. */
  /* Luke, 2026-10-23 (fine-tune): "resolve the spaces between the start
     watching button, the Elite plan message and the threshold alerts" —
     the merged Search-mode hero card's copies of these are class-only
     (no id, see renderAwSetupInfo's own comment) and had NO top margin at
     all, sitting flush under the button. Upgraded from #aw-duration-info
     to the shared .aw-duration-info class so both Manual's #aw-ready-card
     copy and the merged hero copy get the same consistent rhythm, and
     added matching spacing for .aw-boost-cost-info/.aw-defaults-line
     (the latter only ever appeared in the hero card, until the 501-800px
     .aw-mid-note copy below — class selector now covers both). */
  .aw-duration-info { margin-top: 12px; }
  .aw-boost-cost-info { margin-top: 12px; }
  .aw-defaults-line { margin-top: 12px; }
  /* Luke, 2026-10-24: "at the 500-800px breakpoint... I want .aw-note-brand
     and .aw-def-normal to come outside of .aw-sticky-cta and be positioned
     between .aw-selrow and #aw-slab-row... but below 500px return to
     .aw-sticky-cta" — .aw-mid-note (dashboard.html, right after .aw-selrow)
     is the second copy that makes this possible; hidden by default (true
     mobile keeps the .aw-sticky-cta-docked original, desktop keeps the
     inline original) and shown only inside the 501-800px band below. */
  .aw-mid-note { display: none; }
  .submit-btn {
    padding: 9px 22px; background: var(--brand); color: var(--brand-contrast);
    border: none; border-radius: var(--radius); font-family: var(--font);
    font-size: 13px; font-weight: 500; cursor: pointer; transition: opacity .15s;
  }
  .submit-btn:hover { opacity: .88; }
  /* Outlined variant for a secondary action beside a primary .submit-btn (e.g. Resend code). */
  .submit-btn.secondary { background: transparent; color: var(--text); border: 1px solid var(--border2); }
  .submit-btn.secondary:hover { opacity: 1; border-color: var(--muted); }
  /* Add Watch's own final CTA — deliberately bigger and bolder than every
     other .submit-btn on the site, with its own eye icon and a slow
     attention pulse (Luke, 2026-09-11: "a really obvious button that you
     need to click"). flex:1 in .form-actions so it also dominates Cancel
     by sheer width, not just color. */
  .aw-start-btn {
    display: flex; align-items: center; justify-content: center; gap: 9px;
    flex: 1; padding: 15px 24px; background: var(--brand); color: var(--brand-contrast);
    border: none; border-radius: var(--radius-lg); font-family: var(--font);
    font-size: 15px; font-weight: 700; cursor: pointer;
    box-shadow: 0 4px 16px rgba(0,0,0,.28);
    transition: transform .15s, opacity .15s;
    animation: awStartPulse 2.4s ease-in-out infinite;
  }
  .aw-start-btn:hover { opacity: .92; transform: translateY(-1px); }
  .aw-start-btn:disabled { opacity: .75; cursor: default; transform: none; animation: none; }
  .aw-start-btn svg { flex-shrink: 0; }
  /* Rest/peak is scale(1) — the button's native full width, not a size to
     grow past (it already fills its flex row) — so the pulse dips down to
     96% and back rather than growing to ~102% beyond the pane. */
  @keyframes awStartPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(.96); } }
  @media (prefers-reduced-motion: reduce) { .aw-start-btn { animation: none; } }
  .cancel-btn {
    padding: 9px 18px; background: transparent; border: 1px solid var(--dim);
    border-radius: var(--radius); color: var(--muted); font-family: var(--font);
    font-size: 13px; cursor: pointer; transition: all .15s;
  }
  .cancel-btn:hover { color: var(--text); border-color: var(--border2); }
  /* Sits inside the "Your chosen Chase" card itself, top-right corner
     (Luke, 2026-09-11) — was in the section header above the card;
     .wd-card is already position:relative so this just pins to it. */
  .aw-chosen-cancel { position: absolute; top: 10px; right: 10px; padding: 4px; display: flex; align-items: center; justify-content: center; }
  .aw-chosen-cancel svg { width: 11px; height: 11px; }

  /* ── Watch detail pane ── */
  .detail-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 20px; gap: 12px; flex-wrap: wrap; }
  .detail-header-left { display: flex; gap: 16px; align-items: flex-start; flex: 1; min-width: 0; }
  .detail-card-img { height: 220px; width: auto; object-fit: contain; border-radius: 8px; cursor: zoom-in; box-shadow: 0 4px 20px rgba(22,24,29,.18); border: 1px solid var(--border2); flex-shrink: 0; }
  .detail-title { font-family: var(--font); font-size: 22px; font-weight: 600; margin-bottom: 4px; }
  .detail-sub { font-size: 12px; color: var(--text-secondary); }
  .detail-controls { display: flex; gap: 8px; flex-shrink: 0; }
  .ctrl-btn {
    padding: 7px 14px; border-radius: var(--radius); font-family: var(--font);
    font-size: 12px; cursor: pointer; border: 1px solid var(--border2);
    background: transparent; color: var(--text); transition: all .15s;
  }
  .ctrl-btn:hover { background: var(--surface2); }
  .ctrl-btn.danger { color: var(--red); border-color: rgba(231,76,60,.3); }
  .ctrl-btn.danger:hover { background: var(--red-bg); }

  .detail-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 20px; }
  .detail-metric { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-tile); padding: 12px; }
  .dm-label { font-size: 11px; color: var(--muted); margin-bottom: 5px; }
  .dm-val { font-size: 18px; font-family: var(--font); }

  .mkt-price-bar { background: var(--surface); border: 1px solid var(--border); border-top: 3px solid var(--green); border-radius: var(--radius); padding: 14px 16px; }

  /* ── Watch detail v2 (wd-* namespace, isolated from the above) ── */
  /* Deliberately drops its own font-size/color — page-heading + page_title
     (added alongside this class in dashboard.html) now own those, same
     treatment as every other page's title. Only the button-reset bits and
     icon gap stay here. */
  .wd-back { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; background: none; border: none; font-family: inherit; padding: 0; transition: opacity .15s; }
  .wd-back:hover { opacity: .7; }
  /* Explicit widths, not flex-grow ratios/min-width/wrap — that combo was
     producing unpredictable results at real-world widths (including
     mobile, where a flex-column's cross-axis was still align-items:
     flex-start, sizing each child to its own content instead of the full
     row width). Simple and exact instead: 2/3 desktop, 100% mobile. */
  .wd-top-row { display: flex; gap: 10px; align-items: flex-start; }
  .wd-top-row .wd-card { width: calc(66.666% - 5px); margin-bottom: 10px; }
  .wd-top-row .wd-stats { width: calc(33.333% - 5px); margin-bottom: 10px; }
  @media (max-width: 760px) {
    .wd-top-row { flex-direction: column; }
    .wd-top-row .wd-card, .wd-top-row .wd-stats { width: 100%; }
  }
  .wd-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px; margin-bottom: 10px; position: relative; }
  .wd-card.trends { border-color: var(--boost-border); }
  .wd-card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
  .wd-card-title { font-size: 13px; font-weight: 600; color: var(--text); text-transform: uppercase; }
  /* Search mode's "Find it faster" retitled "Boost alert speeds" and moved
     inside #aw-boost-card itself (Luke, 2026-10-23) — same weight/size as
     .wd-card-title, just not uppercase (this one reads as a real title,
     not a numbered step label) and living inside the card's own padding. */
  .aw-boost-card-title { font-size: 14px; font-weight: 700; color: var(--text); margin-bottom: 10px; }
  /* .wd-card-head/.wd-sec-head are flex rows sharing the baseline with a
     button or count next to the title — page_section_title's own
     padding-top (meant to separate a section from whatever's above it)
     would push the title down out of alignment with its sibling here. */
  .wd-card-head .page_section_title, .wd-sec-head .page_section_title { padding-top: 0 !important; }
  .wd-edit { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--boost); cursor: pointer; background: none; border: none; font-family: inherit; padding: 2px; }

  .wd-hero { display: flex; gap: 14px; }
  /* Add Watch's "Your selection" and Watch Detail's hero share this class
     and now match exactly, desktop and mobile (Luke, 2026-10-05: "Watch
     setup (add watch) and watch detail should have parity") — was 150/120
     here with a separate #aw-mkt-art override bumping Add Watch 6px
     taller; unified onto the taller number instead of keeping two. */
  .wd-art { height: 160px; aspect-ratio: 5/7; border-radius: 7px; border: 1px solid var(--border); background: var(--surface); display: flex; align-items: center; justify-content: center; overflow: hidden; flex-shrink: 0; }
  .wd-art img { width: 100%; height: 100%; object-fit: cover; object-position: top; cursor: zoom-in; }
  /* Luke, 2026-10-23: "a decent sized version of the card thumbnail...
     like in the lightbox" for Add Watch's "Your selection" specifically —
     scoped to #aw-market-result only so the real Watch Detail hero (shares
     .wd-art) keeps the 160px parity size from 2026-10-05 above. */
  #aw-market-result .wd-art { height: 350px; border-radius: 5% / 4%; }
  #aw-market-result .wc2-artwrap:has(.wd-art)::after { border-radius: 5% / 4%; }
  /* Luke, 2026-10-23: "take the pokemon card out of the container card" —
     thumbnail and the (now cancel-X-free) info card sit side by side as
     plain siblings, not one nested inside the other's border/background. */
  .aw-selrow { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
  /* Luke, 2026-10-23: "aw-card needs a min-width of 320px" — the info
     card was min-width:0 (shrinks arbitrarily), which could crush its
     text into an uncomfortably narrow column next to the fixed-width art
     thumbnail at in-between (tablet) widths. flex-wrap on .aw-selrow
     (added above) lets the card drop to its own line instead of forcing
     an overflow once 320px genuinely doesn't fit beside the art. */
  .aw-selrow .wd-card { flex: 1; min-width: 320px; }
  /* Luke, 2026-10-23: "when the pokemon card is at the top and the only
     thing on the row... margin:auto to lock to the center" — unconditional
     (not mobile-only): margin:auto on a flex item only has a visible
     centering effect when it's alone on its own flex line with leftover
     horizontal space to distribute (the wrap case above, and mobile's own
     column layout); paired beside the card at wider widths, .wd-card's
     flex:1 already consumes all remaining row width, so auto resolves to
     0 there — safe at every width. */
  #aw-market-result .wc2-artwrap { margin: 0 auto; }
  /* Luke, 2026-10-23 (fine-tune): ".wd-card should also have a height of
     350px" to match the thumbnail — desktop only (≥801px, matching the
     site's own tablet/nav breakpoint — max-width:800px/min-width:801px,
     e.g. the nav hamburger at :2688 — rather than inventing a new one;
     mobile keeps content-sized). */
  @media (min-width: 801px) {
    /* margin-bottom:0 strips .wd-card's own default 10px bottom margin —
       without it the flex row (.aw-selrow) still measured 360px tall even
       with both children's own height set to 350.
       Luke, 2026-10-23: "putting them in one line works to a degree but
       not completely in some responsive layouts" — a hard height:350px +
       overflow-y:auto clipped/scrolled the card's own content whenever it
       genuinely needed more room (e.g. the duration+boost-cost sentence
       wrapping to 2 lines at in-between card widths, ~1000-1100px, where
       the card is narrow but still paired beside the art). min-height
       instead: still exactly 350px in the normal case (nothing to grow
       for), but free to grow past it — together with .aw-area-freq, via
       .aw-hero-row's own align-items:stretch (≥1000px) — instead of
       clipping when content genuinely doesn't fit. */
    .aw-selrow .wd-card { min-height: 350px; margin-bottom: 0; }
  }
  /* "Put the market price in a little tile like in the watch card modal" —
     same boxed .lb-price-row treatment the card lightbox uses, scoped to
     this instance (the real Watch Detail hero's own .wd-mkt-price, and the
     Watches list card's, both stay the plain unboxed text they've always
     been). */
  /* Luke, 2026-10-23: "the height of .wd-mkt-price... keeps changing" —
     the real price (28px text), the skeleton bar (20px) and "Unavailable
     on PulseTCG" (12px text) were all different heights, so the tile
     visibly resized between states. Fixed height (border-box, global * —
     line 226) holds it steady across all three. */
  #aw-market-result .wd-mkt-price { display: inline-flex; align-items: center; gap: 8px; height: 44px; background: var(--surface2); border: 1px solid var(--border2); border-radius: var(--radius); padding: 0 12px; margin-top: 10px; }
  /* Luke, 2026-10-05: "Remove the numeric steps...1. 2. 3. etc" + "we do
     not need to see the card headings... 'SEARCH FOR YOUR CHASE' /
     'YOUR SELECTION' / 'FIND IT FASTER'" — originally mobile-only (below),
     promoted to unconditional 2026-10-23: "these should disappear
     forever" — Manual mode's own remaining numbered headings (2. Find it
     faster, 3. Refine settings) and "Ready to go" now hide at every
     width, same as Search mode's own "Search for your chase"/"Your
     selection" headings, removed outright in an earlier round. */
  .aw-step-num { display:none; }
  .aw-heading-hide-mobile { display:none !important; }
  /* Below 500px, drop the "State:"/"Language:"/"Artist:"/"market" labels —
     just the values — to save width. Originally Add Watch's "Your
     selection" only (scoped to its #aw-mkt-* ids); extended to a plain
     .aw-lbl selector (Luke, 2026-10-04: "replicate on the details page")
     so Watch Detail's hero — which wraps the same labels in .aw-lbl now —
     gets the identical mobile treatment with no separate rule needed. */
  /* Luke, 2026-10-24: "This is a layout that occurs [at ~574px]... I
     think it's due to min-width on the elements... it looks terrible" —
     caught live inside the tablet/"mobile nav" tier (≤800px, per Luke's
     own "800 mobile nav... 500 true mobile" split) but above the 500px
     cutoff this compact thumbnail/merged-box treatment used to be scoped
     to. Below ~586px of container width .aw-selrow's own flex-wrap:wrap
     (base rule) drops .wd-card to its own line once its 320px min-width
     floor no longer fits beside the thumbnail — but the thumbnail was
     still rendering at its full 350px/250px desktop size there (this
     compact treatment hadn't kicked in yet), producing a huge image
     stacked above cramped text instead of the intended lightbox-style
     compact pairing. Widened from 500px to 800px — the same "mobile nav"
     tier the rest of the page now uses (max-width:800px/min-width:801px,
     e.g. the nav hamburger at :2688 — see the 2026-10-24 breakpoint-
     consolidation entry) — so there's no gap between true mobile and the
     point (≥801px, confirmed live) where there's actually enough room
     for the thumbnail and card to sit side by side at full desktop size
     without wrapping. Only the sizing/merged-box rules below move; the
     sticky-bottom-bar/FAB-hiding/footer-hiding block further down stays
     scoped to true ≤500px phones, where fixed-position docking actually
     earns its keep. */
  @media (max-width: 800px) {
    /* Luke, 2026-10-24 (original 500px version): "apply those four
       changes [from the Pokemon/Set/Artist tabs' own card-selection
       lightbox] to the Search tab's hero" — supersedes the earlier "keep
       the Pokemon Card on a row on it's own and keep it at that 350px
       height" request: the lightbox's own mobile treatment (Luke's
       summary, confirmed against its CSS) is "thumbnail shrinks but
       stays BESIDE the text, not full-width stacked above it" — 180px ->
       130px there specifically. Mirrored here: #aw-market-result .wd-art
       drops from the unscoped 350px (base rule above) back to a compact
       height — 182px, since height drives width through this element's
       own aspect-ratio:5/7 (182 * 5/7 ≈ 130px wide, matching the
       lightbox's own exact number). .aw-selrow is NOT forced to
       flex-direction:column — its own base flex-wrap:wrap only drops
       .wd-card to its own line when .wd-card's min-width floor genuinely
       doesn't fit beside the thumbnail; that floor needs lowering here
       too (next rule) or it'd still force the wrap this change is
       specifically undoing. */
    #aw-market-result .wd-art { height: 182px; }
    /* min-width:320px (base rule, tuned for the wider desktop wrap-vs-
       pair decision) is far more than a 130px-wide compact thumbnail
       leaves beside it — floors to 0 instead so the card never wraps
       below at this width; its own text already truncates with an
       ellipsis (name/meta/state/etc., existing rules) rather than
       forcing more room. */
    #aw-market-result .aw-selrow .wd-card { min-width: 0; }
    #aw-market-result .wc2-artwrap:has(.wd-art)::after { border-radius: 5% / 4%; }
    .wc2-artwrap:has(.wd-art)::after { border-radius: 5px; }
    /* Luke, 2026-10-24: the lightbox hides its State:/Language:/Artist:
       labels (and the price tile's "market" label) at this width —
       "apply those four changes" explicitly supersedes the earlier
       "don't remove the .aw-lbl elements" request for this card
       specifically (confirmed with Luke before making this change, since
       it directly reverses that). Both the field labels AND the price's
       "market" label share .aw-lbl in this card's own markup (unlike the
       lightbox, where they're two separate classes), so removing the
       override here hides both the same way the lightbox hides both,
       just via one shared rule instead of two. Watch Detail's own
       identical labels were already hiding at this width via this same
       base rule the whole time, unscoped — this just brings the hero in
       line with it again. */
    .aw-lbl { display:none; }
    /* Luke, 2026-10-24: "bring back... the 'updated {date}'" — #aw-mkt-
       updated carries .aw-lbl on its OWN element (the whole line, not
       just a label prefix the way State:/Language:/Artist: do), so the
       blanket hide above was taking the entire line out, not trimming a
       label off it. Needs #aw-market-result-scoped id+class specificity
       to actually win against that rule, same pattern this file already
       uses elsewhere for exactly this kind of conflict. */
    #aw-market-result #aw-mkt-updated { display: block; }
    /* Luke, 2026-10-24: "align the text sizes so it almost looks the
       same" — compared directly against the lightbox's own mobile sizes
       (.lb-name 16px, .lb-meta 12.5px, .lb-price-val 22px). The State/
       Language/Artist lines (.wd-sub, 12px) already match .lb-row's own
       12px exactly, no change needed there. !important needed on the
       first two — both have an unscoped #aw-market-result-scoped rule
       elsewhere in this file with equal specificity that sits LATER
       (outside this media block), so without it that later rule wins the
       cascade tie despite this media query being the narrower, more
       specific condition — same issue this file hits repeatedly. */
    #aw-market-result .wd-name { font-size: 17px !important; }
    #aw-market-result #aw-mkt-set { font-size: 12.5px !important; }
    #aw-market-result .wd-mkt-price { font-size: 22px; }
    /* Luke, 2026-10-24 (original 500px version): "put the pokemon card in
       the wd-card with the text (like in the modal)" — the lightbox
       nests its thumbnail and text inside ONE shared bordered box
       (.lb-head sits directly inside the modal's own padded card); our
       hero keeps them as plain unbordered siblings at every OTHER width
       (a deliberate earlier choice — "take the pokemon card out of the
       container card"). Can't actually reparent .wc2-artwrap into
       .wd-card with CSS, so the same visual result comes from giving
       .aw-selrow itself the card's own box treatment (background/
       border/radius/padding, copied from .wd-card's base rule) and
       stripping that same treatment back off the now-redundant inner
       .wd-card, so there's one shared box, not two nested ones. */
    #aw-market-result .aw-selrow {
      background: var(--surface); border: 1px solid var(--border);
      border-radius: var(--radius-lg); padding: 14px;
    }
    #aw-market-result .aw-selrow .wd-card {
      background: none; border: none; padding: 0; margin-bottom: 0;
    }
  }
  /* Luke, 2026-10-24: targeted changes for just the 501-800px band (the
     "tablet/mobile nav" tier, excluding true ≤500px mobile): thumbnail
     grows from the compact 182px up to 228px — there's more width to
     spare here than on a real phone — and the quota/defaults lines move
     out of .aw-sticky-cta (where the Start watching button stays) to their
     own line inside .aw-selrow, below both the thumbnail and the card
     (see .aw-mid-note, dashboard.html — a direct child of .aw-selrow,
     flex-basis:100% drops it below the other two flex items onto its own
     row while staying inside .aw-selrow's own merged box) instead of
     directly under the button. Below 500px the compact 182px art and the
     original .aw-sticky-cta-docked lines return (next block down); above
     800px the full 350px art and inline lines return (base rules, outside
     any of these media queries). */
  @media (min-width: 501px) and (max-width: 800px) {
    #aw-market-result .wd-art { height: 228px; }
    #aw-market-result .aw-sticky-cta > .aw-duration-info,
    #aw-market-result .aw-sticky-cta .aw-defaults-line { display: none; }
    #aw-market-result .aw-selrow > .aw-mid-note { display: block; flex-basis: 100%; }
    /* Luke, 2026-10-24: "in this view... you can remove the margin top
       from .aw-duration-info" — the base rule's 12px (meant to clear the
       Start watching button directly above it, the usual case elsewhere)
       is dead space here now that .aw-mid-note sits below the whole flex
       row instead; .aw-selrow's own gap:16px already spaces it from the
       row above. */
    #aw-market-result .aw-mid-note .aw-duration-info { margin-top: 0; }
  }
  @media (max-width: 500px) {
    .wd-art { height: 135px; border-radius: 5px; }
    /* Luke, 2026-10-23: "change the 'Change' in the search field to an
       'x' to clear" on mobile — swaps which of the two children inside
       #aw-searchfield-change is shown; the element itself still just
       calls awChangeSelection() either way. */
    .aw-searchfield-change-text { display: none !important; }
    .aw-searchfield-change-x { display: block !important; }
    /* Luke, 2026-10-05: "Make the Start Watching button and the plan quota
       message a fixed element to the bottom of the mobile view... allow
       the ability to scroll and access the Check live market and other
       elements... but that Start watch button needs to be easy to see and
       access." .aw-sticky-cta wraps just the button + duration-info (not
       the boost-cost line or the Reject/Buy/Offer defaults below it,
       which stay in normal scrollable flow) — see dashboard.html. Only
       docks once its card is actually revealed (a card's been picked);
       before that there's nothing to dock.
       Luke, 2026-10-23: "does this still happen?" — caught live that it
       didn't, for Search mode specifically: this whole block used to be
       scoped to #aw-ready-card only, from when that was the one and only
       Start Watching button on the page. Since the hero-row merge earlier
       this session, Search mode has its own separate copy inside
       #aw-market-result (#aw-ready-card is .aw-only-manual now) — that
       copy was quietly never getting the dock-to-bottom treatment.
       :is()/:has() with both ids fixes every rule in this block at once. */
    :is(#aw-ready-card, #aw-market-result):not([hidden]) .aw-sticky-cta {
      position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
      background: var(--surface); border-top: 1px solid var(--border);
      padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
      box-shadow: 0 -4px 16px rgba(0,0,0,.14);
      display: flex; flex-direction: column; gap: 2px;
      /* Luke, 2026-10-06: "animate up the start watching button from the
         bottom, it's large and takes up lots of real estate" — slides the
         bar in from off-screen instead of snapping into place the instant
         awRevealReady() clears its card's [hidden], so a chunk of UI this
         size arriving doesn't feel abrupt. Safe to animate THIS element
         directly (unlike #aw-ready-card itself, see its own animation:none
         comment below) because .aw-sticky-cta is the position:fixed element,
         not a non-fixed ancestor one is nested inside — it already owns an
         explicit z-index:40 stacking context regardless of animation, so
         there's no containing-block/stacking-context fight with Refine
         settings to reintroduce. */
      animation: aw-sticky-cta-in .32s cubic-bezier(.22,.9,.3,1) both;
    }
    /* Cancels the desktop #aw-duration-info/.aw-duration-info margin-top
       set outside this media query — this flex column's own gap:8px
       already spaces it from the button here, so without this reset
       mobile would stack both (18px total instead of 8px). Class, not
       id, for #aw-market-result's copy — it has no id of its own (see
       renderAwSetupInfo's own comment on the two instances). */
    :is(#aw-ready-card, #aw-market-result):not([hidden]) .aw-sticky-cta .aw-duration-info { margin-top: 0; }
    /* Luke, 2026-10-24: "hugely reduce the size of the fixed bottom
       section... same information, ideally in half the pixel height" —
       .form-actions' own inline margin-top:14px (meant for normal
       scrollable flow, where it's the first thing below the price tile)
       is pure dead space here — the flex column's own gap already
       spaces it from .aw-sticky-cta's top edge. Button shrinks from a
       big pulsing pill (15px padding, 15px font) to a plain compact one
       (no pulse — that animation is itself sized for the bigger button,
       distracting at this scale) — still the obvious, only-red thing on
       the bar, doesn't need extra motion to find. The quota/defaults
       text drops its boxed .aw-duration-note background+border+padding
       (a decent chunk of height on its own for one short sentence) down
       to plain small centered text, matching #aw-defaults-line's own
       existing plain-text treatment right below it. */
    :is(#aw-ready-card, #aw-market-result):not([hidden]) .aw-sticky-cta .form-actions { margin-top: 0 !important; }
    :is(#aw-ready-card, #aw-market-result):not([hidden]) .aw-sticky-cta .aw-start-btn {
      padding: 7px 16px; font-size: 13px; animation: none; box-shadow: none;
      border-radius: 10px;
    }
    /* Matches the compact .aw-start-btn's own height (7px padding +
       13px/1 line-height + border ≈ 38px) so the row reads as one even
       line, not a tall pill beside two short squares. */
    #aw-market-result:not([hidden]) .aw-sticky-cta .aw-quicknav-btn { display: flex; }
    :is(#aw-ready-card, #aw-market-result):not([hidden]) .aw-sticky-cta .aw-duration-note {
      background: none; border: none; padding: 0; line-height: 1.25; padding-top: 4px;
    }
    :is(#aw-ready-card, #aw-market-result):not([hidden]) .aw-sticky-cta #aw-defaults-line {
      font-size: 10px !important; line-height: 1.25; margin-top: 4px;
    }
    @keyframes aw-sticky-cta-in { from { transform: translateY(100%); } to { transform: translateY(0); } }
    @media (prefers-reduced-motion: reduce) {
      :is(#aw-ready-card, #aw-market-result):not([hidden]) .aw-sticky-cta { animation: none; }
    }
    /* Reserves room at the bottom of the scrollable area so the fixed bar
       never covers the last real content (boost-cost line, Reject/Buy/
       Offer defaults, Check live market, Refine settings). Luke, 2026-10-06:
       "on some devices the Refine settings card and other details goes
       behind the Start Watching button" — the old flat 120px guess didn't
       account for the bar's real height varying with the duration-info
       text (can wrap to 2 lines) or a tall safe-area-inset-bottom (home-
       indicator phones). --aw-sticky-cta-h is now measured live by a
       ResizeObserver (_awObserveStickyCta in dashboard.js) and kept
       current across content/viewport changes; 120px only as the fallback
       before JS has measured anything. */
    body:has(#aw-ready-card:not([hidden]), #aw-market-result:not([hidden])) #aw-grid { padding-bottom: calc(var(--aw-sticky-cta-h, 120px) + 16px); }
    /* The bug-report/tutorial FABs live bottom-right on every page and
       would otherwise sit on top of this new bar (Luke: "Temporarily hide
       the bug logging button and the quick tutorial button, when the
       start watching button is visible on mobile"). */
    body:has(#aw-ready-card:not([hidden]), #aw-market-result:not([hidden])) .bug-fab,
    body:has(#aw-ready-card:not([hidden]), #aw-market-result:not([hidden])) .tutorial-fab { display:none !important; }
    /* Luke, 2026-10-05: "Don't show the footer on the add a watch page on
       mobile." Scoped to #pane-add being the active pane so it doesn't
       touch the footer anywhere else. */
    #pane-add.active ~ .site-footer { display:none; }
    /* Luke, 2026-10-05 (annotated screenshot, "HALF"): once the button +
       duration line move into .aw-sticky-cta (above), what's left inside
       #aw-ready-card's own .wd-card — just the defaults title/line — sits
       in the same 14px padding sized for the taller pre-extraction card,
       reading as too much empty space top and bottom. Halved. */
    #aw-ready-card > .wd-card { padding-top:7px; padding-bottom:7px; }
    /* Same screenshot: "Reject 85%... below" wrapped onto a second line.
       Small enough + nowrap to guarantee one line at any phone width
       rather than relying on it narrowly fitting at 12px. */
    #aw-defaults-line { font-size:10.5px !important; white-space:nowrap; }
  }
  .wd-hero-info { flex: 1; min-width: 0; }
  .wd-name-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
  .wd-name { font-size: 17px; font-weight: 700; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Luke, 2026-10-23 (fine-tune): "the pokemon card name... 1/4 larger
     font size" — Add Watch's hero only (Watch Detail's own .wd-name
     keeps its 17px parity size). */
  #aw-market-result .wd-name { font-size: 24px; }
  /* Luke, 2026-10-23 (fine-tune 4): "I don't feel like we have consistency
     in the line heights and we want bold on the values... maybe smaller
     font 11px and cleaner space? Bold values, regular labels" — the
     State/Language/Artist lines (#aw-mkt-extra/#aw-mkt-lang-artist, both
     .wd-sub) had the label (.aw-lbl) and its value as plain text of
     identical weight/color; scoped to the hero only, since Watch Detail's
     own identical markup elsewhere is untouched. The value is whatever
     text ISN'T wrapped in .aw-lbl or <i> (material/promo stay regular
     italic, same as before) — bolding .wd-sub itself and explicitly
     un-bolding its two known plain-text children covers that without
     touching the JS that builds these lines. The number/set line
     (#aw-mkt-set, .wd-sub-line) has no label prefix, so it stays a plain
     subtitle, unbolded, matching Luke's own reference screenshot. */
  /* .card_sub_title (also on these divs) sets font-size/font-weight with
     !important (line ~295) — matching !important here is the only way to
     actually win, plain specificity isn't enough against it. */
  #aw-market-result .wd-sub,
  #aw-market-result .wd-sub-line { font-size: 12px !important; line-height: 1.5; }
  #aw-market-result .wd-sub-line { font-size: 12.5px !important; }
  #aw-market-result #aw-mkt-set { font-size: 13px !important; }
  #aw-market-result .wd-sub { font-weight: 700 !important; color: var(--text); }
  #aw-market-result .wd-sub .aw-lbl { font-weight: 400 !important; color: var(--text-secondary); }
  /* #aw-mkt-updated carries both .wd-sub and .aw-lbl on the SAME element
     (not nested), so the descendant rule above doesn't reach it — Luke:
     "'updated just now' need not be in bold." */
  #aw-market-result .wd-sub.aw-lbl { font-weight: 400 !important; color: var(--text-secondary); }
  #aw-market-result .wd-sub i { font-weight: 400 !important; font-style: italic; color: var(--text-secondary); }
  /* Luke, 2026-10-23 (fine-tune 4): "create that PSA/ACE pill... that
     appears in place of the text here" — the same two-tone grading-slab
     look the company/grade chips use (see .co-chip/.grade-chip), fused
     into one small badge for the "State: " line once a card's graded.
     Built by awGradeBadgeHtml() in dashboard.js. */
  /* Luke, 2026-10-23: "move the relative position of the graded pill up
     2px" — nudged against its own text baseline in the "State: " line. */
  .aw-grade-badge { display: inline-flex; align-items: stretch; border-radius: 4px; overflow: hidden; vertical-align: middle; font-weight: 800; line-height: 1; position: relative; top: -2px; margin-left: 3px; }
  /* Luke, 2026-10-23: "add slight gradient to those bold colours" — the
     company half's flat background (set via --co-color, both here and on
     .co-chip below) becomes a subtle diagonal sheen instead, lighter top-
     left to darker bottom-right. */
  .aw-grade-badge-co { padding: 3px 6px; color: #fff; display: flex; align-items: center; letter-spacing: .02em; font-size: 10px; background: linear-gradient(135deg, color-mix(in srgb, var(--co-color) 100%, white 18%), var(--co-color) 55%, color-mix(in srgb, var(--co-color) 100%, black 20%)); }
  .aw-grade-badge-num { padding: 3px 7px; background: #f1f2f4; color: #17181c; display: flex; align-items: center; font-size: 10px; }
  /* Never wraps to a second line, truncates instead (Luke: "these text
     rows shouldn't ever wrap... just truncate") — these are meant to read
     as one short meta line each, not reflow the card taller. */
  .wd-sub { font-size: 12px; color: var(--text-secondary); margin-top: 3px; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Set/number line: set truncates with an ellipsis when the card's too
     narrow, number never does (min-width:0 on the set span is what lets
     flex actually shrink it below its content width; the number span
     stays flex-shrink:0 so it's always fully readable). */
  .wd-sub-line { display: flex; font-size: 12px; color: var(--text-secondary); margin-top: 3px; line-height: 1.35; min-width: 0; }
  .wd-sub-set { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  /* margin-left (not a leading space in the ::before content) puts the gap
     before the dot — a literal leading space would hit the same
     start-of-line whitespace trim the trailing space below sidesteps. */
  .wd-sub-num { flex-shrink: 0; white-space: nowrap; margin-left: 4px; }
  /* "· " via ::before (not a literal " · " text node) so the dot and its
     spacing never fall victim to CSS's start-of-line whitespace trimming —
     a flex item's own text box trims a LITERAL leading space but never a
     leading non-whitespace character, so starting the generated content
     with the dot itself sidesteps the bug entirely (was rendering as
     "Expedition Base Set29/165" with no separator at all). */
  .wd-sub-num::before { content: "\00b7\0020"; }
  /* Luke, 2026-10-23: "switch the number and the set around" — Add Watch's
     "Your selection" only; swapped the dot separator along with them
     (number leads now, set trails and can truncate) rather than leave a
     leading dot in front of the number. Watch Detail's own hero and the
     Watches list keep the original set-then-number order, unscoped. */
  #aw-mkt-set .wd-sub-num { margin-left: 0; }
  #aw-mkt-set .wd-sub-num::before { content: none; }
  #aw-mkt-set .wd-sub-set { margin-left: 4px; }
  #aw-mkt-set .wd-sub-set::before { content: "\00b7\0020"; }
  /* Luke, 2026-10-09: "crude horrible font!" — Sora is deliberately scoped
     to only a few areas app-wide (dashboard.html's own comment: "dashboard
     page + shared alert/watch card components only for now"), and this
     28px/700 price never made that list, so it's always rendered in the
     plain system-font fallback — fine at normal body-text sizes, visibly
     crude at this size/weight. Opted in directly rather than touching the
     app-wide --font/body scoping, same narrow-carve-out pattern the modal/
     footer fix already uses elsewhere in this file. */
  .wd-mkt-price { font-family: 'Sora', var(--font); font-size: 28px; font-weight: 700; color: var(--text); line-height: 1; margin-top: 7px; display: flex; align-items: center; gap: 6px; }
  /* Luke, 2026-10-10: 28px (shouting) -> 19px -> 14px (too quiet, "I need
     it to be obviously, but not over dominating") -> 18px, then "make the
     text red and make it 2/3rd current font size and bold" — 12px ≈
     18px * 2/3. Red + bold carries the "obviously" now instead of size. */
  .wd-mkt-unavailable { font-size: 12px; font-weight: 700; color: var(--red); }
  /* Luke, 2026-10-23 (fine-tune 3): "do not write the word 'checking'...
     put a loading animation or skeleton effect in the market price area"
     — a shimmering bar fills #aw-mkt-price's own box while a graded
     lookup (awGetSlabPrice) is in flight, same height/shape as the real
     price text it's about to be replaced by. */
  .aw-price-skeleton { gap: 0; }
  .aw-skeleton-bar {
    display: block; width: 90px; height: 20px; border-radius: 4px;
    background: linear-gradient(100deg, var(--border2) 30%, var(--surface3, var(--border)) 50%, var(--border2) 70%);
    background-size: 200% 100%;
    animation: awSkeletonShimmer 1.2s ease-in-out infinite;
  }
  @keyframes awSkeletonShimmer { 0% { background-position: 150% 0; } 100% { background-position: -50% 0; } }
  @media (prefers-reduced-motion: reduce) { .aw-skeleton-bar { animation: none; } }
  .wd-mkt-note { font-size: 12px; color: var(--dim); font-style: italic; margin-top: 10px; }
  /* Luke, 2026-10-09: "move the price up 3px, leave the Updated text in the
     same place" — .wd-mkt-price's own margin-top above just dropped 10px→7px
     to move IT up; since that also pulls this next sibling up 3px for free
     (shorter gap above it now), add the 3px back here (.wd-sub's own 3px + 3
     = 6px) so #aw-mkt-updated's on-screen position doesn't move at all. */
  #aw-mkt-updated { margin-top: 6px; }
  #aw-market-result #aw-mkt-updated { font-size: 11px !important; }
  .wd-mkt-price.dim { color: var(--muted); font-weight: 500; }
  .wd-mkt-price .market-price-icon { width: 18px; height: 18px; flex: 0 0 18px; }
  .wd-mkt-label { font-size: 13px; font-weight: 500; color: var(--muted); margin-left: -3px; padding-top: 8px; }

  /* Single column on desktop (was 2, side by side) — .wd-stats sits next
     to .wd-card in .wd-top-row there, already squeezed to ~1/3 width, so
     a 2-up grid inside that was cramping each tile further. Styled
     closer to .dash-tile per Luke's feedback: same background, radius,
     and a bigger value size than the old plain stat tile had. On mobile
     .wd-top-row drops to a full-width single column (see its own media
     query above), so .wd-stats has the room back for its tiles to sit
     side by side again instead of stacking unnecessarily. */
  .wd-stats { display: grid; grid-template-columns: 1fr; gap: 8px; margin-bottom: 10px; }
  @media (max-width: 760px) { .wd-stats { grid-template-columns: repeat(2, minmax(0,1fr)); } }
  .wd-stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px 16px; }
  .wd-stat .l { font-size: 11px; color: var(--muted); letter-spacing: .08em; margin-bottom: 6px; }
  .wd-stat .v { font-size: 16px; font-weight: 600; color: var(--text); line-height: 1.3; }
  .wd-stat-time { font-size: 14px; font-weight: 400; }
  /* Status dot folded into the DURATION stat, replacing the standalone
     "● Active" line that used to sit under the hero price (Luke,
     2026-10-04: "We no longer need to see the -Active either... captured
     within the .wd-stat with the DURATION in it"). Same dot/color
     language as the line it replaces. */
  .wd-stat-status { display: flex; align-items: center; gap: 5px; margin-top: 4px; font-size: 12px; font-weight: 400; color: var(--green); }
  .wd-stat-status .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); display: inline-block; flex-shrink: 0; }
  .wd-stat-status.paused { color: var(--muted); }
  .wd-stat-status.paused .dot { background: var(--muted); }

  /* margin-bottom on the base rule (was buy-only) — with the per-row edit
     slider sitting directly under each row, the old buy-only margin left
     inconsistent, tighter spacing above Negotiate's slider than Buy
     now's. */
  .wd-thr { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 11px 13px; border-radius: 9px; margin-bottom: 8px; }
  .wd-thr.buy { background: var(--green-bg); border-left: 3px solid var(--green); }
  .wd-thr.neg { background: var(--amber-bg); border-left: 3px solid var(--amber); }
  .wd-thr.reject { background: var(--brand-bg); border-left: 3px solid var(--brand); }
  .wd-thr .k { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: .2px; }
  .wd-thr.buy .k { color: var(--green); }
  .wd-thr.neg .k { color: var(--amber); }
  .wd-thr.reject .k { color: var(--brand); }
  .wd-thr .h { font-size: 11px; color: var(--muted); margin-top: 1px; }
  .wd-thr .r { text-align: right; flex-shrink: 0; }
  .wd-thr .v { font-size: 19px; font-weight: 700; color: var(--text); }
  .wd-thr .p { font-size: 12px; }
  .wd-thr.buy .p { color: var(--green); }
  .wd-thr.neg .p { color: var(--amber); }
  .wd-thr.reject .p { color: var(--muted); }
  /* Manual-mode (isCustom) watches, editing: the field sits inline where
     the static price (.v) normally is — a real rounded/bordered text
     field (matching the rest of the app's inputs), not a bare underline. */
  .wd-thr-manual-field { align-items: center; gap: 3px; background: var(--surface); border: 1px solid var(--border2); border-radius: var(--radius); padding: 5px 10px; }
  .wd-thr-manual-prefix { font-size: 14px; font-weight: 500; color: var(--muted); }
  .wd-thr-manual-input { width: 95px; background: none; border: none; text-align: right; font-family: inherit; font-size: 15px; font-weight: 600; color: var(--text); outline: none; padding: 0; }

  /* Slider sits directly under its own threshold row (not both grouped
     below in a shared grid, per Luke's feedback), toggled together by
     toggleThresholdEdit() — see the matching #threshold-edit-buy-/-neg-
     ids in renderDetail(). Accent colours match the row above them
     (green/amber), not the generic brand red the shared-grid version
     used. */
  .wd-thr-slider { padding: 8px 13px 2px; margin: -4px 0 8px; }
  .wd-thr-slider.reject { margin-bottom: 0; }
  .wd-thr-slider .disp { font-size: 13px; font-weight: 600; margin-bottom: 6px; }
  .wd-thr-slider.buy .disp { color: var(--green); }
  .wd-thr-slider.neg .disp { color: var(--amber); }
  .wd-thr-slider.reject .disp { color: var(--brand); }
  .wd-thr-slider.reject .rej-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
  .wd-thr-slider.reject.reject-off .disp { color: var(--muted); }
  .wd-thr-slider.reject.reject-off input[type=range] { opacity: .35; pointer-events: none; }
  .wd-thr.reject.off { background: var(--surface2); border-left-color: var(--dim); }
  .wd-thr.reject.off .k, .wd-thr.reject.off .v { color: var(--muted); }
  /* Offer's read-only summary row, disabled — same treatment as Reject's
     (Luke, 2026-10-15: "make offer say 'Off / not applied' just like
     reject does when disabled"). */
  .wd-thr.neg.off { background: var(--surface2); border-left-color: var(--dim); }
  .wd-thr.neg.off .k, .wd-thr.neg.off .v, .wd-thr.neg.off .p { color: var(--muted); }
  /* Fully custom-styled, not accent-color — styling the unselected track
     (per Luke's ask) silently drops the browser's own accent-color
     rendering entirely (thumb colour AND vertical centering both broke
     together, confirming this the hard way).
     The fill: JS can't set inline styles on a pseudo-element (no CSSOM
     access to ::-webkit-slider-runnable-track), so the input's own
     background can't be the fill — a track pseudo paints its own box on
     top of it regardless. The actual mechanism is a CSS custom property
     (--fill, a px value set by _paintSliderFill in dashboard.js), which
     — unlike inline styles — does cascade into pseudo-elements, referenced
     directly in the track's own gradient below.
     Vertical centering: the input's own height matches the thumb (18px),
     not the thin 6px visual track. A negative margin-top on the thumb
     (tried first, -6px) depends on exactly how each browser positions a
     thumb by default relative to a shorter track, which isn't consistent
     enough to hand-tune reliably (Luke still saw it a few px off after
     that attempt). With thumb height == input height and no margin-top
     at all, the thumb simply fills the input's own box — and the
     shorter track, centered within that same box by the browser's own
     default layout, ends up centered on it for free. */
  .wd-slider { -webkit-appearance: none; appearance: none; width: 100%; height: 18px; cursor: pointer; background: transparent; --fill: 0px; }
  .wd-slider.buy { --thumb-color: var(--green); }
  .wd-slider.neg { --thumb-color: var(--amber); }
  /* Same ascending left-to-right direction as Buy/Negotiate (a mirrored
     version shipped first, then reverted same-day — Luke, 2026-09-11,
     correctly called it out as confusing: all three sliders are really
     one continuous "how far below market" axis, so they should all
     move the same way. Right = further below market = more extreme,
     consistently across all three; Reject just continues past Buy's
     own ceiling of 50. */
  .wd-slider.reject { --thumb-color: var(--brand); }
  .wd-slider::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: linear-gradient(to right, var(--thumb-color) 0px, var(--thumb-color) var(--fill), var(--bg) var(--fill), var(--bg) 100%); }
  .wd-slider::-moz-range-track { height: 6px; border-radius: 3px; background: linear-gradient(to right, var(--thumb-color) 0px, var(--thumb-color) var(--fill), var(--bg) var(--fill), var(--bg) 100%); border: none; }
  /* A same-colour-as-fill thumb (tried first) was reported as "no handle
     to drag" — technically present but not visually distinct enough
     against the fill, especially past ~50%. A light disc with a thick
     coloured ring reads as an obvious handle at any position, matching
     familiar slider-thumb conventions (e.g. iOS).
     --thumb-y is the one knob for nudging vertical position by hand — a
     plain px value, negative moves the handle up, positive moves it
     down, 0 is "however the browser centers it by default" (input height
     already matches thumb height, see .wd-slider above, so 0 should
     already be correct — this exists so a visual mismatch can be tuned
     directly by eye without touching the sizing/centering mechanism
     itself). WebKit and Firefox can end up needing different values here
     since they don't necessarily agree on the default down to the pixel —
     that's what the two separate rules below are for. */
  .wd-slider { --thumb-y: -6px; }
  .wd-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%; cursor: pointer; background: var(--surface); border: 3px solid var(--thumb-color); box-shadow: 0 1px 3px rgba(0,0,0,.35); margin-top: var(--thumb-y); }
  .wd-slider::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; cursor: pointer; background: var(--surface); border: 3px solid var(--thumb-color); box-shadow: 0 1px 3px rgba(0,0,0,.35); transform: translateY(var(--thumb-y)); }
  .wd-thr-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; flex-wrap: wrap; }

  .wd-price-warn-text { font-size: 13px; color: var(--red); font-weight: 600; margin-bottom: 10px; line-height: 1.4; }
  .wd-price-warn-row { display: flex; align-items: center; gap: 8px; }
  .wd-manual-note { font-size: 11px; color: var(--muted); }
  .wd-manual-note button { background: none; border: none; color: var(--boost); cursor: pointer; font-family: inherit; font-size: 11px; padding: 0; margin-left: 4px; text-decoration: underline; }
  .wd-manual-edit { display: flex; align-items: center; gap: 8px; margin-top: 8px; }

  /* Three small tiles (watchers / rank / alerts fired) replacing the old
     single big-number+rank-pill lede — same "big number, quiet caption"
     language, just three of them side by side instead of one combined
     block, since alerts-fired moved here from the retired stats pair. */
  .wd-trend-cards { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 8px; }
  .wd-trend-tile { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px 10px; text-align: center; display: flex; flex-direction: column; justify-content: center; }
  .wd-trend-tile .num { font-family: var(--font); font-size: 22px; font-weight: 700; color: var(--text); line-height: 1; display: flex; align-items: center; justify-content: center; gap: 4px; }
  .wd-trend-tile .cap { font-size: 11px; color: var(--muted); margin-top: 7px; line-height: 1.3; }
  /* Top-10 rank gets a "hot" treatment — simple popularity signal without
     a full numeric scale, per Luke's ask. */
  .wd-trend-tile.hot { border-color: var(--amber); background: var(--amber-bg); }
  .wd-trend-tile.hot .num, .wd-trend-tile.hot .cap { color: var(--amber); }
  .wd-scale-lbl { font-size: 11px; color: var(--muted); letter-spacing: .3px; margin-bottom: 8px; }
  .wd-freq-note { font-size: 12px; color: var(--muted); text-align: center; margin-top: 12px; }
  .wd-tiers { display: flex; margin-bottom: 6px; }
  .wd-tiers > div { flex: 1; text-align: center; font-size: 11px; font-weight: 600; letter-spacing: .4px; color: var(--dim); }
  .wd-tiers > div.on { color: var(--text); }
  .wd-tiers-br { display: none; }
  @media (max-width: 500px) { .wd-tiers-br { display: block; } }
  .wd-grow { display: flex; gap: 8px; align-items: stretch; }
  .wd-pill { flex: 1; display: flex; border: 1.5px solid var(--border2); border-radius: 10px; overflow: hidden; background: var(--surface); }
  /* Each of the 4 cells always gets its own colour on the blue->purple
     scale (see tierColors in JS) — reached/current stages show it at full
     strength, stages not yet reached show the same colour dimmed rather
     than a flat neutral grey. */
  .wd-cell { position: relative; flex: 1; height: 36px; display: flex; align-items: center; justify-content: center; border-right: 1px solid var(--border); color: #fff; opacity: .3; }
  .wd-cell:last-child { border-right: none; }
  .wd-cell.lit { opacity: 1; }
  /* Add Watch setup only — clicking a rung above the tier's own starting
     position pre-selects "apply this many boosts once the watch exists"
     (see renderAwSetupInfo/awSelectBoostLevel). Cells at/below the tier's
     own rung aren't selectable at all — that speed is already free.
     Scoped to .aw-pill (not the base .wd-cell.off rule, which the
     read-only Watch Detail meter also uses and shouldn't change) so an
     unselected rung here reads as a flat, theme-aware grey "not active,
     costs something" block instead of a dimmed version of the tier's own
     colour — the dimmed-colour treatment didn't make clear there was a
     cost at all. */
  .aw-pill .wd-cell.off { opacity: 1; color: var(--dim); }
  .wd-cell-selectable { cursor: pointer; }
  .wd-cell-selectable:hover { opacity: .85; }
  /* Dim ("not yet unlocked") but clickable, not inert — Luke, 2026-09-02:
     tapping this should offer to buy a boost, not just show a "Not
     enough boosts" dead-end tooltip (see awBuyBoostForLevel). */
  .aw-pill .wd-cell-disabled { opacity: .4 !important; cursor: pointer; }
  /* One bolt (the same size as the "this is your speed" bolt on a lit
     cell, not a separate tiny badge icon) plus the flat per-step cost —
     replaces the decorative bolt entirely on an unselected cell, rather
     than showing a big bolt AND a hard-to-notice small cost underneath. */
  .wd-cell-cost { display: flex; align-items: center; gap: 3px; font-size: 13px; font-weight: 700; }
  .wd-plus { width: 44px; border: 1.5px solid var(--boost-border); border-radius: 10px; background: var(--surface2); color: var(--boost); display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; font-family: inherit; }
  .wd-spds { display: flex; margin-top: 7px; }
  .wd-spds > div { flex: 1; text-align: center; font-size: 11px; font-weight: 600; color: var(--dim); }
  .wd-spds > div.on { color: var(--text); }
  .wd-pad { padding-right: 52px; }
  .wd-nudge { background: var(--boost-bg); border-radius: 10px; padding: 11px 5px; }
  .wd-nudge-row { display: flex; gap: 9px; align-items: flex-start; }
  .wd-nudge-row > i { color: var(--boost); flex-shrink: 0; margin-top: 1px; }
  .wd-nudge .t { font-size: 12px; font-weight: 600; color: var(--boost); }
  .wd-nudge .h { font-size: 11px; color: var(--muted); margin-top: 2px; line-height: 1.45; }
  /* Add Watch's boost nudge only (Luke, 2026-10-05: "centrally align this
     text", then "The boost icon should be on the first line with the
     text") — icon sits beside the first line only, second line centered
     underneath. Scoped to #aw-boost-info so Watch Detail's own longer
     nudge (icon + text + a full-width button below) keeps its original
     left-aligned row layout, which still suits that one better. */
  #aw-boost-info .wd-nudge-row { flex-direction: column; align-items: center; text-align: center; gap: 1px; }
  #aw-boost-info .wd-nudge-title-row { display: flex; align-items: center; gap: 1px; }
  /* Luke, 2026-10-05: "bolt should be boost blue" — the bolt svg
     (fill="currentColor") sits beside .t inside .wd-nudge-title-row, not
     as .wd-nudge-row's direct child the old `.wd-nudge-row > i` rule
     expects (that one's also an <i> selector; this icon's an <svg>), so it
     was inheriting the row's default text color instead. */
  #aw-boost-info .wd-nudge-title-row svg { color: var(--boost); flex-shrink: 0; }
  /* .h's own margin-top (meant to sit below a left-aligned multi-line
     block elsewhere) double-spaces on top of the row gap above, and its
     1.45 line-height was tuned for the longer Watch Detail nudge, not this
     short single line (Luke, 2026-10-05: "this line height can be
     reduced"). */
  #aw-boost-info .wd-nudge .h { margin-top: 0; line-height: 1.3; }
  .wd-btn-boost { width: 100%; margin-top: 11px; height: 34px; border: 1px solid var(--boost-border); border-radius: 8px; background: var(--surface2); color: var(--boost); font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; font-family: inherit; }
  .wd-btn-boost .soon { font-size: 11px; background: var(--surface); color: var(--muted); padding: 1px 6px; border-radius: 20px; font-weight: 400; margin-left: 2px; }
  /* Luke, 2026-10-24: "in the instances that #aw-slab-row and
     #aw-boost-card are on the same row, I want to add an additional
     description" — "I know that between the 688px(inclusive) and 999px
     (inclusive) they are on the same row" (measured live, not derived from
     a single existing breakpoint — #aw-slab-row's own width behaves
     differently across the 501px/801px/999px bands it's scoped in). Hidden
     by default; shown only in that exact range below.
     "centrally align the .wd-nudge text" + "remove the search icon" — no
     icon left to space from (markup dropped it), so just center the
     remaining text block instead of the component's usual left-aligned
     icon+text row. */
  .aw-slab-nudge { display: none; margin-top: 10px; }
  .aw-slab-nudge .wd-nudge-row { justify-content: center; text-align: center; }
  /* Luke, 2026-10-25: "on this Manual watch setup page the nudge should
     be 688px+ (no 800px limitation)" — dropped the 800px upper bound from
     the previous 688-800px band; shows at every width ≥688px now, no
     ceiling. Manual mode's own equivalent of .aw-slab-nudge above. */
  .aw-manual-slab-nudge { display: none; margin-top: 10px; }
  .aw-manual-slab-nudge .wd-nudge-row { justify-content: center; text-align: center; }
  @media (min-width: 688px) {
    .aw-manual-slab-nudge { display: block; }
  }
  @media (min-width: 688px) and (max-width: 999px) {
    .aw-slab-nudge { display: block; }
    /* The height-match (#aw-slab-row to match #aw-boost-card) lives further
       down, right after the existing 801-999px block that sets
       .aw-area-freq's align-items — same specificity, later source order
       needed to actually win the cascade tie (this file's recurring
       gotcha); see that block's own comment. */
    #aw-slab-row { display: flex; flex-direction: column; }
    #aw-slab-row .aw-slab-fields { flex: 1; }
  }

  .wd-lrow { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 0; border-top: 1px solid var(--border); }
  .wd-lrow:first-child { border-top: none; padding-top: 0; }
  /* Add Watch's Listing Criteria card: taller rows than Watch Detail's own
     use of .wd-lrow, and the first row keeps its top padding (unlike
     Watch Detail's, which the rule above zeroes) since here it's not
     hugging a heading directly above it. */
  .aw-area-criteria .wd-lrow { padding: 12px 0; }
  /* .wd-lrow:first-child (above) never matches here — the keyword inputs'
     .form-grid is the card's actual first child, not this row — so the
     "no border on the first row" rule needs an explicit marker instead. */
  .aw-area-criteria .wd-lrow-noborder { border-top: none; }
  .aw-area-criteria .wd-lrow:last-child { padding-bottom: 6px; }
  .wd-lrow .k { font-size: 11px; color: var(--muted); flex-shrink: 0; }
  .wd-lrow .v { font-size: 12px; color: var(--text); text-align: right; }
  .wd-lrow .v.none { color: var(--dim); }

  .wd-sec-head { display: flex; align-items: baseline; justify-content: space-between; margin: 16px 2px 10px; }
  .wd-sec-head .n { font-size: 11px; color: var(--muted); }
  .wd-filters { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
  .wd-fchip { font-size: 11px; padding: 4px 10px; border-radius: 20px; border: 1px solid var(--border2); color: var(--muted); cursor: pointer; white-space: nowrap; background: none; font-family: inherit; }
  .wd-fchip.on { background: var(--text); color: var(--bg); border-color: var(--text); font-weight: 600; }

  .wd-more { width: 100%; height: 36px; border: 1px solid var(--border2); border-radius: 8px; background: var(--surface); color: var(--text); font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; font-family: inherit; }

  .wd-menu { position: relative; flex-shrink: 0; }
  .wd-menu-trigger { border: none; background: none; padding: 2px 4px; border-radius: 6px; cursor: pointer; color: var(--muted); line-height: 0; }
  .wd-menu-trigger:hover { background: var(--surface); color: var(--text); }
  .wd-menu.open .wd-menu-trigger { background: var(--surface); color: var(--text); }
  .wd-menu-pop { position: absolute; top: 26px; right: 0; width: 170px; background: var(--surface2); border: 1px solid var(--border2); border-radius: var(--r-control); box-shadow: 0 10px 28px -8px rgba(22,24,29,.16); padding: 5px; z-index: 30; display: none; }
  .wd-menu.open .wd-menu-pop { display: block; }
  .wd-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 9px; border-radius: 6px; border: none; background: none; font-size: 12px; color: var(--text); cursor: pointer; text-align: left; font-family: inherit; }
  .wd-menu-item:hover { background: var(--surface); }
  .wd-menu-item.danger { color: var(--red); }
  .wd-menu-sep { height: 1px; background: var(--border); margin: 4px 5px; }

  .listing-lozenges { display: flex; flex-direction: column; gap: 8px; }
  .listing-lozenge { position: relative; display: flex; align-items: flex-start; gap: 10px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 38px 10px 10px; transition: border-color .15s; }
  .listing-lozenge:hover { border-color: var(--border2); }
  .listing-ignore-btn { position: absolute; top: 8px; right: 8px; background: none; border: 1px solid var(--border2); border-radius: var(--radius); color: var(--muted); cursor: pointer; padding: 2px 7px; font-size: 11px; line-height: 1.4; transition: all .15s; }
  .listing-ignore-btn:hover { border-color: var(--red); color: var(--red); }


  /* Mobile drawer */
  .sidebar-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 50; }
  .sidebar-backdrop.show { display: block; }
  aside.drawer-open { transform: translateX(0) !important; }

  /* ── Mobile nav icons ── */
  .mob-nav-icons { display: none; align-items: center; gap: 2px; padding-left: 8px; }
  .mob-nav-btn { background: none; border: none; cursor: pointer; color: var(--muted); padding: 7px 6px; border-radius: var(--radius); transition: color .15s; display: flex; align-items: center; justify-content: center; line-height: 1; }
  .mob-nav-btn:hover { color: var(--text); }
  .mob-nav-btn.active { color: var(--brand); }

  @media (max-width: 800px) {
    .mob-burger { display: block; }
    .desktop-controls { display: none; }
    .header-nav { display: none; }
    .logo { border-right: none; padding-right: 0; margin-right: 0; }
    .logo-desktop { display: none; }
    .logo-mobile { display: block; height: 70px; object-fit: contain; }
    main { margin-left: 0; }
    aside {
      width: 280px;
      transform: translateX(-100%); transition: transform .25s ease;
      z-index: 51; border-right: 1px solid var(--border2);
    }
    .detail-header { flex-direction: column; }
    .detail-header-left { flex-direction: column; align-items: center; text-align: center; width: 100%; }
    .detail-card-img { height: 160px; }
    .detail-controls { width: 100%; justify-content: center; }
    .detail-grid { grid-template-columns: repeat(2, 1fr) !important; }
    .summary-grid { grid-template-columns: repeat(2, 1fr); }
    .mob-nav-icons { display: flex; }
    .mob-watched-section { display: block; }
    .form-grid, .aw-name-num-grid { grid-template-columns: 1fr !important; }
    .threshold-row { grid-template-columns: 1fr; }
    .pane { padding: 14px; }

    /* iOS Safari zooms the whole page in when a focused text input computes
       below 16px, and doesn't zoom back out on blur — forcing 16px here
       (including over inline `style="font-size:...px"` attributes, hence
       !important) stops the zoom from triggering at all. */
    input[type="text"], input[type="search"], input[type="number"],
    input[type="email"], input[type="password"], input:not([type]),
    select, textarea {
      font-size: 16px !important;
    }
  }

  /* ── Settings ── */
  /* .pane already centers itself up to 1200px (see the shared `.pane`
     rule), but that's wider than this page's own 900px card block wants
     to be — .settings-wrap re-centers the heading + cards together within
     that, the same way `.pricing-wrap` does for Support/Pricing/Referral.
     Centering .settings-cards alone (tried first) left the "Settings"
     heading flush against the pane's left edge while the cards centered
     independently below it, visibly misaligned from the title. */
  .settings-wrap { max-width: 900px; margin: 0 auto; }
  /* Two independent flex columns on desktop — not a grid. A shared grid
     row is sized to the TALLEST card placed in it from EITHER column
     (align-items:start only controls a card's position within that
     already-tall row, not the row's own height), so pairing a short card
     opposite a tall one leaves a visible gap below the short one before
     the next card starts. Flex columns pack each side tightly against its
     own content regardless of the other — this was tried as a grid once
     already and reverted for exactly this gap bug. Desktop order is
     therefore just DOM order within each `.settings-col` (see the two
     wrappers in dashboard.html); no per-card CSS needed there at all.
     Mobile wants a *different*, cross-column order (see the media query
     below) that plain two-column-stacking can't produce — `display:
     contents` on `.settings-col` there removes the wrapper from layout
     entirely (its cards become direct flex items of `.settings-cards`),
     which is what lets a single flex column with per-card `order` reorder
     across the old column boundary without reintroducing the grid's
     shared-row-height problem (order only changes sequence, never
     sizing). */
  .settings-cards { display: flex; gap: 16px; align-items: flex-start; }
  .settings-col { flex: 1 1 340px; min-width: 0; display: flex; flex-direction: column; gap: 16px; }
  /* Luke, 2026-09-15: mobile breakpoint is <801px app-wide, not 760px;
     and "make sure all cards are full width on mobile" — the base rule's
     align-items:flex-start (needed on desktop so a .settings-col doesn't
     stretch to match its sibling column's height) also meant every card
     here sized to its own content instead of the container's width once
     display:contents turned them into direct flex items of a column-flex
     .settings-cards, which is what produced the ragged left/right edges. */
  @media (max-width: 800px) {
    .settings-cards { flex-direction: column; align-items: stretch; }
    .settings-col { display: contents; }
    #settings-card-avatar       { order: 1; }
    #settings-card-handle       { order: 2; }
    #settings-card-profile      { order: 3; }
    #discord-connect-card       { order: 4; }
    #instagram-connect-card     { order: 5; }
    #settings-card-subscription { order: 6; }
    #settings-card-review       { order: 7; }
    #settings-card-privacy      { order: 8; }
    #settings-card-password     { order: 9; }
    #s-phone-card                { order: 10; }
  }
  .settings-card { position: relative; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-card); padding: 18px; }
  /* Tightened (Luke, 2026-09-24: "the headings... seem to have too much
     top padding") — .page_section_title's own padding-top:10px !important
     is a shared rule used all over the app (dashboard tiles, add-watch
     sections, etc.), so this scopes the override to Settings cards only,
     same established pattern as .wd-card-head/.win-eyebrow's own
     page_section_title overrides elsewhere in this file. */
  .settings-card .settings-card-title.page_section_title { padding-top: 0 !important; }
  .settings-card-title { margin-bottom: 10px; }
  .settings-card-title-brand { display: flex; align-items: center; gap: 8px; }
  .settings-card-title-brand svg { flex-shrink: 0; }
  .settings-card .field { margin-bottom: 14px; }
  .settings-avatar-row { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; }
  /* No title on this card any more — just a bigger avatar (Luke,
     2026-09-24: "the avatar card doesn't need a title... just make the
     avatar bigger"). */
  .settings-avatar-img { width: 110px; height: 110px; border-radius: 50%; background: var(--surface2); border: 1px solid var(--border2); flex-shrink: 0; }
  .settings-avatar-handle { font-size: 18px; font-weight: 600; color: var(--muted); margin-bottom: 10px; }
  .settings-avatar-firstname { font-size: 30px; font-weight: 700; color: var(--text); margin-bottom: 2px; }

  /* Google account email on its own line with its "G" mark, so linked-via-
     Google reads as a fact rather than a parenthetical (Luke, 2026-09-24:
     "emphasise this point"). */
  .settings-google-email-row { display: flex; align-items: center; gap: 8px; font-size: 13px; }
  .settings-google-email-row svg { flex-shrink: 0; }

  /* Privacy card's alert-email override — its own visually distinct
     sub-block (Luke, 2026-09-24: "the alternative email layout looks
     crap. it needs space and structure"), separated from the fixed
     Account email field above by a real border rather than just margin. */
  .privacy-subsection { border-top: 1px solid var(--border); margin-top: 18px; padding-top: 16px; margin-bottom: 18px; }
  .settings-remove-btn { display: flex; align-items: center; gap: 6px; color: var(--red); border-color: var(--red); }
  .settings-remove-btn:hover { background: rgba(var(--red-rgb, 220, 38, 38), .08); }

  /* Reward badge — top-right corner of a card, advertising the one-time
     reward for filling it in (Luke, 2026-09-24: "make it clear that some
     elements will be rewarded... show the reward in the top right corner
     of the cards"). Colored by asset type rather than one flat color —
     Boosts blue/purple (the same --tutorial-accent already used for
     "Boost" iconography elsewhere) with the bolt glyph, Watch Tokens red
     with the eye glyph (Luke: "Watches in red, with an eye icon" — same
     path the Watches page heading itself uses). Once earned, swaps to a
     green "Earned" pill instead of just disappearing (Luke: "put a green
     pill with a tick in as a replacement to the boost/watch detail"), via
     the .srb-pending/.srb-earned pair — see _applyProfile's own toggle. */
  .settings-reward-badge {
    position: absolute; top: 14px; right: 16px;
    font-size: 11px; font-weight: 700; letter-spacing: .01em;
    padding: 3px 9px; border-radius: 999px; white-space: nowrap;
    display: flex; align-items: center; gap: 4px;
    transition: opacity .5s ease;
  }
  /* "Earned" is only ever a brief confirmation, not a permanent badge (Luke,
     2026-09-24) — the JS flashes .earned for a few seconds then adds this
     to fade it out before hiding the badge for the rest of the session. */
  .settings-reward-badge.fade-out { opacity: 0; }
  .settings-reward-badge svg { flex-shrink: 0; }
  .settings-reward-badge.asset-boosts { background: rgba(var(--tutorial-accent-rgb), .15); color: var(--tutorial-accent); }
  .settings-reward-badge.asset-watch_tokens { background: rgba(232, 37, 26, .12); color: var(--brand); }
  .settings-reward-badge .srb-pending { display: flex; align-items: center; gap: 4px; }
  .settings-reward-badge .srb-earned { display: none; align-items: center; gap: 4px; }
  .settings-reward-badge.earned { background: rgba(46, 204, 113, .15); color: var(--green); }
  .settings-reward-badge.earned .srb-pending { display: none; }
  .settings-reward-badge.earned .srb-earned { display: flex; }

  /* Simple pill toggle for boolean Settings fields (e.g. collector-stats
     opt-out) — no label inside the switch itself, paired with adjacent text. */
  .settings-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 14px; }
  /* Unscoped (Luke, 2026-09-24) — used identically outside toggle rows too
     (Alerts & correspondence's own sub-heading, the phone card's caption). */
  .field-label { font-size: 13px; font-weight: 600; color: var(--text); }
  .field-sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
  .settings-toggle { position: relative; display: inline-block; width: 40px; height: 22px; flex-shrink: 0; cursor: pointer; }
  .settings-toggle input { opacity: 0; width: 0; height: 0; }
  .settings-toggle .settings-toggle-track { position: absolute; inset: 0; background: var(--border2); border-radius: 999px; transition: background .15s; }
  .settings-toggle .settings-toggle-track::before { content: ''; position: absolute; width: 16px; height: 16px; left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: transform .15s; }
  .settings-toggle input:checked + .settings-toggle-track { background: var(--green); }
  .settings-toggle input:checked + .settings-toggle-track::before { transform: translateX(18px); }
  .pcqueue-badge { display: inline-block; margin-left: 6px; padding: 1px 7px; border-radius: 999px; background: rgba(168,85,247,.15); color: #a855f7; font-size: 10px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; vertical-align: middle; }

  .review-star-row { display: flex; align-items: center; gap: 3px; margin-bottom: 12px; }
  .review-star { position: relative; display: inline-block; font-size: 28px; line-height: 1; cursor: pointer; user-select: none; }
  .review-star-bg, .review-star-fill { display: block; }
  .review-star-bg { color: var(--border2); }
  .review-star-fill { position: absolute; top: 0; left: 0; width: 0%; overflow: hidden; color: var(--amber); white-space: nowrap; }
  .review-star-row.disabled .review-star { cursor: default; }
  .review-rating-label { margin-left: 10px; }
  .review-textarea {
    display: block; width: 100%; min-height: 90px; resize: vertical;
    background: var(--surface2); border: 1px solid var(--border2);
    border-radius: var(--radius); padding: 10px 12px;
    color: var(--text); font-family: var(--font); font-size: 13px;
    outline: none; transition: border-color .15s; box-sizing: border-box;
  }
  .review-textarea:focus { border-color: var(--brand); }
  .review-textarea::placeholder { color: var(--dim); }
  .review-textarea:disabled { opacity: .6; cursor: not-allowed; }

  /* ── Log ── */
  .log-line { font-size: 11px; padding: 4px 0; border-bottom: 1px solid var(--border); display: flex; gap: 10px; line-height: 1.5; }
  .log-line:last-child { border-bottom: none; }
  .log-ts { color: var(--dim); flex-shrink: 0; }
  .log-card { color: var(--muted); flex-shrink: 0; }
  .log-msg { color: var(--text); }
  .log-line.buy .log-msg { color: var(--green); }
  .log-line.negotiate .log-msg { color: var(--amber); }
  .log-line.error .log-msg { color: var(--red); }

  /* ── Overlay ── */
  .overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.7); z-index: 100; align-items: center; justify-content: center; overflow-y: auto; }
  .overlay.show { display: flex; }
  /* Two+ modals open together (Luke, 2026-09-15: "allow multiple modals on
     at a time... display them together" rather than queued or fully
     overlapping) — _restackOpenOverlays() (dashboard.js) splits the
     viewport into one equal-height band per open overlay via inline
     top/height, so this only needs to let a tall modal scroll within its
     own now-shorter band instead of overflowing it. */
  .overlay.overlay-stacked { padding: 12px 0; box-sizing: border-box; }
  .modal { background: var(--surface); border: 1px solid var(--border2); border-radius: var(--radius-lg); padding: 24px; width: 480px; max-width: 95vw; }
  /* One modal type scale, taken from the Open Beta welcome modal (Luke, 2026-09-21): heading 21px / 800 / tight
     tracking / 1.2 line height, message copy 14px. The Basket modal keeps its own compact scale (its table is
     carefully arranged) — see the #shop-basket-overlay block below. */
  .modal-title { font-family: var(--font); font-size: 21px; font-weight: 800; letter-spacing: -.01em; line-height: 1.2; margin-bottom: 6px; }
  .modal-sub, .modal .modal-sub.card_sub_title { font-size: 14px !important; font-weight: 400 !important; line-height: 1.5; color: var(--text-secondary); margin-bottom: 20px; }
  #shop-basket-overlay .modal-title { font-size: 18px; font-weight: normal; letter-spacing: normal; line-height: normal; margin-bottom: 4px; }
  #shop-basket-overlay .modal-sub, #shop-basket-overlay .modal .modal-sub.card_sub_title { font-size: 12px !important; line-height: normal; }

  /* Competition "Reveal the winners" modal (2026-09-08) — announcement
     card, then swapped in place for the reveal sequence itself and
     finally the win/lose outcome, all inside the same overlay so there's
     no jarring modal-closes-then-a-new-one-opens flicker between stages.
     Built on the shared .modal/.modal-title/.modal-sub (same base every
     other overlay in this file uses — Luke: "honor our existing modal
     styling") with only the reveal-specific extras layered on top:
     centered heading, the top-right ✕, the prize rows, and the win/lose
     footer. Sora comes from #comp-reveal-overlay's own scope rule above
     (this overlay lives outside #pane-shop's DOM, same fix as the
     basket modal needed) rather than anything set here. */
  .reveal-modal { position: relative; width: 420px; max-width: 92vw; text-align: center; }
  .reveal-modal-wide { width: 460px; }
  /* Shared top-right ✕ for any .modal-based overlay that isn't one of
     dashboard.html's own static overlays (those inline the same five
     properties directly) — reveal modal and the first-watch-reward
     modal both use this rather than each declaring their own copy. */
  .modal-close-btn { position: absolute; top: 14px; right: 14px; background: none; border: none; color: var(--muted); font-size: 18px; cursor: pointer; line-height: 1; padding: 4px; }
  .reveal-modal-icon { font-size: 40px; line-height: 1; margin-bottom: 10px; }
  /* modal-title/modal-sub are left-aligned by default elsewhere — center
     just this instance, and give the header a bit more breathing room
     before whatever comes next (Luke: "central heading, a little more
     padding below the modal header text"). */
  /* Heading weight matches the Open Beta welcome modal (Luke, 2026-09-21: the style he likes) */
  .reveal-modal .modal-title { text-align: center; margin-bottom: 8px; font-weight: 800; }
  .reveal-modal .modal-sub { text-align: center; margin-bottom: 24px; }
  .reveal-draw-date { font-size: 11.5px; color: var(--dim); }
  .reveal-cta-btn { margin-top: 4px; background: var(--brand); color: #fff; border: none; border-radius: var(--radius); padding: 12px 22px; font-family: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; }
  /* Prizes prepend()ed in as they reveal (worst prize first) — each new
     row takes the top slot of a plain top-to-bottom list, pushing the
     previous ones down, so the grand prize (revealed last) is the one
     still sitting at the very top once the sequence finishes — Luke:
     "show the winner at the top." */
  .reveal-rows { display: flex; flex-direction: column; gap: 8px; min-height: 40px; }
  .reveal-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; background: var(--surface2); border: 1px solid var(--border2); border-radius: var(--r-nested); padding: 12px 14px; animation: revealRowIn .4s ease both; }
  .reveal-row.you { background: var(--brand-bg); border-color: var(--brand-border); box-shadow: 0 0 0 1px var(--brand-border); }
  .reveal-row-prize { font-size: 12.5px; color: var(--muted); flex-shrink: 0; }
  .reveal-row-winner { font-size: 13.5px; font-weight: 600; color: var(--text); text-align: right; }
  .reveal-row-winner .handle { font-weight: 400; color: var(--muted); }
  .reveal-row-winner .ticket { font-weight: 400; color: var(--dim); font-variant-numeric: tabular-nums; }
  .reveal-row-winner .none { font-weight: 400; color: var(--dim); font-style: italic; }
  @keyframes revealRowIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
  @media (prefers-reduced-motion: reduce) { .reveal-row { animation: none; } }
  .reveal-outcome { margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--border2); text-align: center; }

  /* First-watch signup reward modal (2026-09-10) — same .modal base +
     .topup-modal-icon.credits badge the Buy Credits picker already
     uses, so a first-time reward doesn't invent yet another visual
     language of its own. */
  .fwr-modal { position: relative; width: 420px; max-width: 92vw; text-align: center; }
  .fwr-modal .modal-title { text-align: center; margin-bottom: 8px; }
  .fwr-modal .modal-sub { text-align: center; margin-bottom: 4px; }
  .fwr-body { font-size: 13px; color: var(--text-secondary); line-height: 1.6; margin: 16px 0 22px; }
  .fwr-body strong { color: var(--text); }
  .fwr-cta-btn { display: block; width: 100%; background: var(--brand); color: #fff; border: none; border-radius: var(--radius); padding: 12px 22px; font-family: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; }
  /* Secondary CTA on the beta-welcome modal (added 2026-09-24) — same shape
     as .fwr-cta-btn, deliberately quieter (outline, not filled) since
     "Add your first watch" stays the primary action. */
  .beta-welcome-secondary-btn { display: block; width: 100%; background: transparent; color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); padding: 11px 22px; margin-bottom: 16px; font-family: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer; }
  .beta-welcome-secondary-btn:hover { border-color: var(--border-strong); background: var(--surface2); }

  /* Top-up sheet (boost/token purchase) — icon badge + accent-colored
     bundle rows, replacing plain grey .act-btn rows that "seemed very
     crude" with no tie to what's actually being bought (Luke, 2026-09-02).
     Same boost-purple language .wd-btn-boost/.wd-nudge already use
     elsewhere, plus a brand-red equivalent for watch tokens, rather than
     inventing a third palette just for this modal. */
  /* Insufficient-credits copy is now two sentences (Luke, 2026-09-24:
     "You have tried to purchase a 'Boost'... You need {X} Credits for
     this.") joined by a literal \n in the JS/Python strings that set
     this — needs pre-line or that newline just collapses to a space. */
  #boost-modal-sub { white-space: pre-line; }
  .topup-modal-icon[hidden] { display: none; }
  .topup-modal-icon { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 14px; }
  .topup-modal-icon svg { width: 22px; height: 22px; }
  .topup-modal-icon.success { background: var(--green-bg, rgba(34,197,94,.12)); color: var(--green); box-shadow: 0 0 0 1px var(--green-border, rgba(34,197,94,.35)); }
  .topup-modal-icon.neutral { background: var(--surface2); color: var(--text-secondary); box-shadow: 0 0 0 1px var(--border2); }
  .topup-modal-icon.boost { background: var(--boost-bg); color: var(--boost); box-shadow: 0 0 0 1px var(--boost-border); }
  .topup-modal-icon.token { background: var(--brand-bg); color: var(--brand); box-shadow: 0 0 0 1px var(--brand-border); }
  /* Same brand-red badge as .token, for a destructive confirm's icon
     (bulk-delete alerts etc.) — Luke: "a red trashcan icon... as per
     the style of other modals," not a new one-off treatment. */
  .topup-modal-icon.danger { background: var(--brand-bg); color: var(--brand); box-shadow: 0 0 0 1px var(--brand-border); }
  /* First-competition-entry modal's badge — same brand-tinted circle as
     .token, holding the ticket icon. */
  .topup-modal-icon.ticket { background: var(--brand-bg); color: var(--brand); box-shadow: 0 0 0 1px var(--brand-border); }
  .topup-bundle-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 14px; border-radius: 10px; border: 1px solid transparent; background: var(--surface2); color: var(--text); cursor: pointer; font-family: inherit; font-size: 13px; font-weight: 500; text-align: left; transition: background .12s, border-color .12s, transform .08s; }
  /* Was just a 1px lift — barely visible (Tom, 2026-09-24: "not much of
     a hover state... look at what we do in other places"). Same
     surface2->surface + border brightening idiom as every other neutral
     list row/card in the app (.wc2-menu-item, .alert-menu-item,
     .wcm-add, etc). Only affects rows with no color variant of their
     own (.boost/.token's own :hover, more specific, still wins there). */
  .topup-bundle-row:hover { transform: translateY(-1px); background: var(--surface); border-color: var(--border2); }
  .topup-bundle-row:active { transform: translateY(0) scale(.98); }
  .topup-bundle-icon { flex-shrink: 0; width: 30px; height: 30px; border-radius: 8px; display: flex; align-items: center; justify-content: center; }
  .topup-bundle-icon svg { width: 15px; height: 15px; }
  .topup-bundle-label { flex: 1; }
  .topup-bundle-price { display: inline-flex; align-items: center; gap: 4px; font-weight: 700; }
  .topup-bundle-price svg { width: 15px; height: 15px; flex-shrink: 0; }
  .topup-bundle-row.boost { background: var(--boost-bg); border-color: var(--boost-border); }
  .topup-bundle-row.boost:hover { background: color-mix(in srgb, var(--boost-bg) 60%, var(--boost-border) 40%); }
  .topup-bundle-row.boost .topup-bundle-icon { background: var(--boost); color: var(--brand-contrast, #fff); }
  .topup-bundle-row.boost .topup-bundle-price { color: var(--boost); }
  .topup-bundle-row.token { background: var(--brand-bg); border-color: var(--brand-border); }
  .topup-bundle-row.token:hover { background: color-mix(in srgb, var(--brand-bg) 60%, var(--brand-border) 40%); }
  .topup-bundle-row.token .topup-bundle-icon { background: var(--brand); color: var(--brand-contrast, #fff); }
  .topup-bundle-row.token .topup-bundle-price { color: var(--brand); }
  .topup-modal-icon.credits { background: var(--brand-bg); color: var(--brand); box-shadow: 0 0 0 1px var(--border2); }
  .topup-modal-icon.basket { background: var(--brand-bg); color: var(--brand); box-shadow: 0 0 0 1px var(--brand-border); }
  /* Post-upgrade confirmation modal (Tom, 2026-09-16: "there is no
     confirmation on the homepage... after they upgrade") — same diamond
     as the Dashboard's own Membership upgrade callout icon. */
  .topup-modal-icon.tier { background: var(--brand-bg); color: var(--brand); box-shadow: 0 0 0 1px var(--brand-border); }
  /* Row itself dropped from brand-red back to neutral (Tom, 2026-09-24:
     "lets not have green buttons. just go without colour" — a colored
     row read as its own button/CTA rather than a plain list item), but
     a genuinely discounted price stays green ("and have the prices stay
     green" — was red, which read as a danger/disabled state; green
     already means "good deal" everywhere else in the app, e.g.
     wc2-pill.buy). No row-level background/border override, so it just
     inherits the plain .topup-bundle-row/.topup-bundle-icon styling.
     Green is scoped to .discounted (added only when
     _isRealBundleDiscount() is true, dashboard.js) — Tom, 2026-09-25:
     "if there is no strikethrough price... the number shouldnt be
     green as that looks like its on sale." A plain, undiscounted price
     stays the row's own default text color. */
  .topup-bundle-row.credits .topup-bundle-price { color: var(--text); }
  .topup-bundle-row.credits .topup-bundle-price.discounted { color: var(--green); }
  .topup-bundle-row.credits .topup-bundle-price .face { color: var(--dim); text-decoration: line-through; font-weight: 400; margin-right: 2px; }

  /* ── Dismiss reason modal ── */
  /* ── Language filter buttons ── */
  .lang-btn { display:inline-flex; align-items:center; gap:6px; background: var(--surface2); border: 1px solid var(--border2); border-radius: 20px; padding: 5px 13px; font-size: 12px; color: var(--muted); cursor: pointer; font-family: inherit; transition: background .15s, border-color .15s, color .15s; }
  .lang-btn.active { background: var(--brand-bg); border-color: var(--brand-border); color: var(--brand); }
  .lang-btn:hover:not(.active) { border-color: var(--muted); color: var(--text); }
  .lang-btn img { display:block; border-radius:2px; flex-shrink:0; }
  @media (max-width:860px) { .lang-label { display:none; } .lang-btn { padding: 5px 9px; } }

  .dismiss-group-label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 14px 0 6px; }
  .dismiss-group-label:first-child { margin-top: 0; }
  .dismiss-chip { background: var(--surface2); border: 1px solid var(--border2); border-radius: 20px; padding: 5px 12px; font-size: 12px; color: var(--text); cursor: pointer; transition: background .15s, border-color .15s; font-family: inherit; }
  .dismiss-chip:hover { background: var(--border2); border-color: var(--muted); }

  /* ── Bug report FAB ── */
  /* Tinted-brand pill, same visual language as the amber "eBay/Vinted
     didn't return results" banner and .dash-upgrade's brand-bg/brand-border
     treatment elsewhere in this file — no new colors, just the existing
     brand tokens applied to a floating button instead of an inline banner.
     z-index 60 sits above the sidebar (40/41) but below every modal/overlay
     (100+), so it never fights the boost/confirm/bug-report overlays for
     clicks once one's open. */
  .bug-fab {
    position: fixed; right: 20px; bottom: 20px; z-index: 60;
    display: flex; align-items: center; gap: 7px;
    background: var(--brand); border: 1px solid var(--brand);
    color: var(--brand-contrast); border-radius: 999px; padding: 10px 16px 10px 12px;
    font-family: var(--font); font-size: 12px; font-weight: 500;
    cursor: pointer; transition: background .15s, border-color .15s, color .15s;
    box-shadow: 0 10px 28px -8px rgba(22,24,29,.16);
  }
  .bug-fab:hover { background: var(--surface); border-color: var(--brand); color: var(--brand); }
  .bug-fab-icon { width: 15px; height: 15px; line-height: 1; flex-shrink: 0; }
  .bug-fab-icon svg { display: block; width: 100%; height: 100%; fill: currentColor; }
  @media (max-width: 800px) {
    .bug-fab { padding: 12px; right: 14px; bottom: 14px; }
    .bug-fab-label { display: none; }
  }

  /* ── First-visit dashboard tutorial (added 2026-09-15) ──
     Invoke button sits directly above the bug-report FAB, same shape/
     collapse-to-icon behaviour at the same 800px breakpoint so the two
     stack consistently on every screen size. Pulses gently so a brand
     new user notices it without it screaming for attention forever —
     stops pulsing entirely once has_seen_tutorial is true server-side
     (see tutorialFabSeen() in dashboard.js), leaving a plain static FAB
     anyone can still use to replay the tour anytime. */
  .tutorial-fab {
    position: fixed; right: 20px; bottom: 76px; z-index: 60;
    display: flex; align-items: center; gap: 7px;
    background: var(--surface); border: 1px solid var(--brand);
    color: var(--brand); border-radius: 999px; padding: 10px 16px 10px 12px;
    font-family: var(--font); font-size: 12px; font-weight: 600;
    cursor: pointer; transition: background .15s, color .15s;
    box-shadow: 0 10px 28px -8px rgba(22,24,29,.16);
  }
  .tutorial-fab:hover { background: var(--brand); color: var(--brand-contrast); }
  .tutorial-fab.pulsing { animation: tutorial-fab-pulse 2.6s ease-out infinite; }
  .tutorial-fab-icon { width: 15px; height: 15px; line-height: 1; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 12px; border-radius: 50%; border: 1.5px solid currentColor; }
  @keyframes tutorial-fab-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(232,37,26,.45), 0 10px 28px -8px rgba(22,24,29,.16); }
    70%  { box-shadow: 0 0 0 11px rgba(232,37,26,0), 0 10px 28px -8px rgba(22,24,29,.16); }
    100% { box-shadow: 0 0 0 0 rgba(232,37,26,0), 0 10px 28px -8px rgba(22,24,29,.16); }
  }
  @media (max-width: 800px) {
    .tutorial-fab { padding: 12px; right: 14px; bottom: 68px; }
    .tutorial-fab-label { display: none; }
  }

  /* ── Back-to-top FAB (Luke, 2026-10-06: "a little round button with an
     up arrow located in the bottom left of each page that has a long
     scroll bar") — bottom-LEFT, mirroring the bug-report/tutorial FABs'
     bottom-right stack so neither pair fights the other for space. `main`
     is the real scroll container on every page (see its own comment
     above), so visibility and the click target are both driven off main's
     scrollTop, not window — see _scrollTopFabSync()/scrollMainToTop() in
     dashboard.js. Hidden rather than shown-but-empty below the threshold,
     so a short page never displays a button with nowhere useful to go. */
  .scroll-top-fab {
    position: fixed; left: 20px; bottom: 20px; z-index: 60;
    width: 42px; height: 42px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    background: var(--surface); border: 1px solid var(--border2);
    color: var(--text); cursor: pointer;
    box-shadow: 0 10px 28px -8px rgba(22,24,29,.16);
    opacity: 0; pointer-events: none; transform: translateY(6px);
    transition: opacity .18s, transform .18s, background .15s, border-color .15s, color .15s;
  }
  .scroll-top-fab.visible { opacity: 1; pointer-events: auto; transform: translateY(0); }
  .scroll-top-fab:hover { background: var(--brand); border-color: var(--brand); color: var(--brand-contrast); }
  .scroll-top-fab svg { width: 18px; height: 18px; display: block; }
  @media (max-width: 800px) {
    .scroll-top-fab { left: 14px; bottom: 14px; width: 38px; height: 38px; }
    .scroll-top-fab svg { width: 16px; height: 16px; }
  }
  /* Same reasoning as .bug-fab/.tutorial-fab's own mobile rule above: the
     Add Watch sticky Start-watching bar spans the full width at the very
     bottom once a card's picked, so this would otherwise sit underneath it. */
  @media (max-width: 500px) {
    body:has(#aw-ready-card:not([hidden])) .scroll-top-fab { display: none !important; }
  }

  /* The tour card — positioned live, in JS, right next to whatever the
     current step is talking about (_tutorialPositionCard computes the
     target element's real getBoundingClientRect() and places the card
     beside it, clamped to stay on-screen), with .tutorial-arrow pointing
     from the card's edge back at the target. Works identically on
     mobile/desktop since it's derived from wherever that element
     actually is on THIS layout, not a hardcoded position per breakpoint.
     left/top are set inline by JS; this block is just the shape/shadow
     and the width cap + a no-target fallback corner. */
  /* Everything tutorial-related sits at the very top of the stack — Tom,
     2026-09-15: "It should probably always have z space above everything
     to be safe" (found hiding behind Market Data at the old z-index:200,
     which sat below several real page elements with their own z-index). */
  .tutorial-backdrop, .tutorial-card, .tutorial-arrow, .tutorial-highlight { z-index: 99990; }
  /* Semi-opaque full-screen backdrop, shown only for "modal" steps (the
     welcome/closing steps, which have no specific target) — Tom: "Make a
     semi opaque dark screen with the model in the middle... this was it
     really obvious where to start and look." Content steps stay
     backdrop-less so the real page underneath is still fully usable. */
  .tutorial-backdrop {
    display: none; position: fixed; inset: 0; background: rgba(0,0,0,.6);
  }
  .tutorial-backdrop.show { display: block; }
  /* Deliberately built on --tutorial-* tokens, not the normal --surface/
     --text/--border2 ones — Luke: "make the tooltips light in dark mode
     to make them stand out and viseversa for light mode." Those tokens
     are defined INVERTED from the site's own theme (see :root /
     :root[data-theme="dark"] up top), so the card is always the
     opposite of whatever's behind it. */
  .tutorial-card {
    display: none; position: fixed;
    width: 320px; max-width: calc(100vw - 32px);
    background: var(--tutorial-bg); border: 1px solid var(--tutorial-border); border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg); padding: 18px 20px;
    transition: left .25s ease, top .25s ease;
  }
  .tutorial-card.show { display: block; }
  /* Modal steps: centered on screen over the backdrop, ignoring left/top
     (JS never sets them when there's no target — see .no-target). */
  .tutorial-card.no-target {
    left: 50%; top: 50%; right: auto; bottom: auto;
    transform: translate(-50%, -50%);
  }
  .tutorial-arrow {
    display: none; position: absolute; width: 14px; height: 14px;
    background: var(--tutorial-bg); border: 1px solid var(--tutorial-border);
    transform: rotate(45deg);
  }
  /* Card sits BELOW the target -> arrow on the card's top edge, pointing up. */
  .tutorial-arrow.arrow-up { top: -8px; border-right: none; border-bottom: none; }
  /* Card sits ABOVE the target -> arrow on the card's bottom edge, pointing down. */
  .tutorial-arrow.arrow-down { bottom: -8px; border-left: none; border-top: none; }
  .tutorial-card-kicker { display: flex; align-items: center; justify-content: center; margin-bottom: 8px; }
  .tutorial-dots { display: flex; gap: 5px; }
  .tutorial-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--tutorial-border); }
  .tutorial-dot.active { background: var(--tutorial-accent); }
  /* Plain close (✕), not a "Skip tour" text button — Tom: "Remove skip
     tour button and just have a close button." Same shape as every
     other modal's close-X in this file (.modal-close-btn) for consistency. */
  .tutorial-card-close { position: absolute; top: 14px; right: 14px; font-size: 18px; line-height: 1; color: var(--tutorial-muted); background: none; border: none; cursor: pointer; font-family: inherit; padding: 2px 4px; }
  .tutorial-card-close:hover { color: var(--tutorial-text); }
  .tutorial-card-title { font-size: 15px; font-weight: 700; margin-bottom: 5px; color: var(--tutorial-text); }
  .tutorial-card.no-target .tutorial-card-title { font-size: 18px; text-align: center; }
  /* text-wrap: balance (Tom, 2026-09-18: "prevent orphan") — stops a
     lone short word wrapping onto its own last line by balancing line
     lengths across the paragraph instead of greedily filling each one. */
  .tutorial-card-text { font-size: 13px; color: var(--tutorial-text-secondary); line-height: 1.55; margin-bottom: 14px; text-wrap: balance; }
  .tutorial-card.no-target .tutorial-card-text { font-size: 13.5px; text-align: center; }
  /* Closing step only (Tom, 2026-09-18: "make this bigger and more
     exciting, they just did something great") — same 🎉 size/spacing as
     .reveal-modal-icon on a real card win, plus a bigger title/text than
     the rest of the tour gets, matching that step's already-larger
     no-target styling but scaled up further. */
  .tutorial-card-icon { font-size: 40px; line-height: 1; text-align: center; margin-bottom: 8px; }
  .tutorial-card.celebrate .tutorial-card-title { font-size: 22px; }
  .tutorial-card.celebrate .tutorial-card-text { font-size: 14.5px; }
  /* One-time free-Credit claim button on the closing step (Tom,
     2026-09-18) — full-width like the primary Next/Done button below
     it, since they're never both meaningful actions at once (Done is
     still the way to actually close the modal either way). */
  .tutorial-claim-box { margin-bottom: 10px; }
  .tutorial-claim-box .tutorial-btn { width: 100%; }
  .tutorial-claim-box .tutorial-btn:disabled { opacity: .6; cursor: default; }
  /* Tom, 2026-09-18: "this text needs to be a lot clearer to see" — a
     solid pill (not plain colored text sitting in the card's own body
     copy) so the one actionable instruction on a gated step reads as
     unmistakably different from the descriptive text above it. */
  .tutorial-card-hint {
    display: flex; align-items: center; justify-content: center; font-size: 12.5px;
    color: #fff; font-weight: 700; margin-bottom: 14px;
    background: var(--tutorial-accent); border-radius: 999px; padding: 8px 14px;
  }
  /* Back is display:none (not visibility:hidden) on step 1 — the hidden-
     but-still-occupying-space version left Next's flex:1 filling only the
     leftover width next to Back's reserved slot, so it rendered off-
     centre instead of spanning/centring properly (Tom, 2026-09-16:
     "buttons off centre"). */
  .tutorial-card-actions { display: flex; gap: 8px; align-items: center; justify-content: center; }
  .tutorial-btn { flex: 1; padding: 9px 14px; border-radius: var(--radius); font-family: inherit; font-size: 12.5px; font-weight: 700; cursor: pointer; }
  .tutorial-btn.primary { background: var(--tutorial-accent); color: #fff; border: 1px solid var(--tutorial-accent); }
  /* Subtle back/forward arrows (Tom, 2026-09-18: "help users click
     through the options even if they don't click the areas we want
     them to... subtle back and forward arrows? maybe lose the back
     button and rely on the arrows") — small, muted, not competing with
     the primary Next/Done button for attention. The forward one is the
     escape hatch for a gated step (see tutorialForceNext), so it only
     ever appears alongside the hint text, never next to the primary
     button. */
  .tutorial-nav-arrow {
    flex: 0 0 auto; width: 32px; height: 32px; padding: 0;
    display: flex; align-items: center; justify-content: center;
    background: none; color: var(--tutorial-muted); border: 1px solid var(--tutorial-border);
    border-radius: 50%; cursor: pointer;
  }
  .tutorial-nav-arrow:hover { color: var(--tutorial-text); border-color: var(--tutorial-accent); }
  .tutorial-nav-arrow svg { width: 15px; height: 15px; }

  /* Rotating border-beam glow on whichever real element the current step
     wants attention on — a class toggle on the element itself, so it
     naturally adapts to wherever that element actually sits on mobile
     vs. desktop. Was an outline+box-shadow pulse floating OUTSIDE the
     element (Tom, 2026-09-18: "actually highlight the border of the
     element? not put the ring over the element... stroke the actual
     border with the animation... make all the purple rings the same
     rotation animation we use for 'chase your first card'") — now the
     exact same technique as .glow-border (border-beam-spin, defined
     above near "Chase your first card") just in the tutorial's own
     purple, traced flush against the element's own real border/corners
     via border-radius: inherit + the content-box mask-exclude trick,
     instead of a separate ring with a gap around it. */
  .tutorial-highlight {
    position: relative;
    z-index: 1;
  }
  .tutorial-highlight::before {
    content: ""; position: absolute; inset: -1px; z-index: -1;
    border-radius: inherit; padding: 2px;   /* Tom, 2026-09-18: "make the animated ring a little thicker" (was 1.5px) */
    background: conic-gradient(from var(--border-beam-angle), transparent 0deg, transparent 270deg, var(--tutorial-accent) 315deg, var(--tutorial-accent) 337deg, transparent 360deg);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    mask-composite: exclude;
    animation: border-beam-spin 3.5s linear infinite;
    pointer-events: none;
  }
  @media (prefers-reduced-motion: reduce) {
    .tutorial-highlight::before { animation: none; }
  }
  /* Nav-icon highlight (the destination tab a nav-required step is
     pointing at) needs its own style — the outline+offset above overflows
     OUTSIDE the 80px-tall fixed header, so on the top nav its top edge got
     clipped by the viewport and its bottom blended into the header's own
     red bottom border, leaving only the two side edges visibly reading as
     "just side lines" (Tom, 2026-09-16: "surround the menu item with red,
     not just have side lines. The top and bottom look cut off."). An
     INSET ring stays fully inside the tab's own box, so nothing overflows
     the header to be clipped. Also only shown on hover of the tutorial
     card itself (see the mouseenter/mouseleave wiring near
     _tutorialSetNavHighlight) — Tom: "can we not highlight the next step
     in the nav unless we hover over the tooltip? It's confusing what is
     being selected" — so this class alone draws nothing; it's added to
     the target element only while the card is hovered. */
  .tutorial-highlight-nav {
    outline: none !important;
    animation: tutorial-highlight-pulse-inset 1.4s ease-in-out infinite;
  }
  @keyframes tutorial-highlight-pulse-inset {
    0%, 100% { box-shadow: inset 0 0 0 2px var(--tutorial-accent), inset 0 0 0 6px rgba(var(--tutorial-accent-rgb),.35); }
    50%      { box-shadow: inset 0 0 0 2px var(--tutorial-accent), inset 0 0 0 6px rgba(var(--tutorial-accent-rgb),0); }
  }

  /* Nav "point at the next tab" steps (Tom, 2026-09-18: "remove the ring
     around alerts/wins and instead make the actual text and icon in the
     nav pulse purple") — always-on (not hover-gated like the ring above),
     recolors the tab's own text + icon straight to the tutorial accent
     since the icon's stroke="currentColor" rides along with `color` for
     free. Always shown while its step is active, unlike the hover-only
     ring. */
  .tutorial-nav-pulse {
    font-weight: 700;
    text-decoration: underline;
    animation: tutorial-nav-color-pulse 1.4s ease-in-out infinite;
  }
  @keyframes tutorial-nav-color-pulse {
    0%, 100% { color: var(--muted); }
    50%      { color: var(--tutorial-accent); }
  }

  /* Fake watch/alert/win rows injected only while the tour is active —
     built by pushing plausible objects straight into the real
     state.watches/state.alerts/state.wins and calling the real
     renderWatchesPage()/renderAlerts()/renderArchive(), never a hand-
     built replica (Tom: "It's really important the interface is exactly
     the same. I dont want you creating stuff from scratch. I just want
     you to use what we have."). The ribbon is the only tutorial-specific
     marker, added after the fact so it doesn't need its own template. */
  .tutorial-fake-badge {
    position: absolute; top: -8px; left: 12px; z-index: 2;
    background: var(--brand); color: var(--brand-contrast); font-size: 10px; font-weight: 700;
    letter-spacing: .03em; text-transform: uppercase; padding: 2px 8px; border-radius: 999px;
  }

  /* Dropzone for the bug-report screenshot field — file input/button sit
     inside it too, so dragging over the button still counts as a hit.
     Inset box-shadow instead of a border so the highlight doesn't shift
     layout by adding width. */
  .bug-image-dropzone { border-radius: var(--radius); transition: background .15s, box-shadow .15s; padding: 2px; margin: -2px; }
  .bug-image-dropzone.drag-over { background: var(--brand-bg); box-shadow: 0 0 0 2px var(--brand) inset; }

  /* ── Image lightbox ── */
  .img-lightbox { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.88); z-index: 200; align-items: center; justify-content: center; cursor: zoom-out; }
  .img-lightbox.show { display: flex; }
  .img-lightbox img { max-width: 90vw; max-height: 88vh; object-fit: contain; border-radius: 10px; box-shadow: 0 8px 48px rgba(0,0,0,.7); }
  /* PokePulse card renders only (openLightbox's isCard flag) — an
     elliptical radius that matches the card's own printed corner curve,
     rather than the generic fixed-px rounding every other lightbox image
     (e.g. a listing's own photo) gets. */
  .img-lightbox img.card { border-radius: 4% / 3.2%; }

  /* ── Market lookup ── */
  .lookup-btn {
    display: flex; align-items: center; gap: 6px;
    padding: 8px 14px; background: transparent;
    border: 1px solid var(--border2); border-radius: var(--radius);
    color: var(--muted); font-family: var(--font); font-size: 12px;
    cursor: pointer; transition: all .15s; margin-top: 10px;
  }
  .lookup-btn:hover { color: var(--text); border-color: var(--brand); }
  .lookup-btn.loading { color: var(--brand); border-color: var(--brand); animation: pulse 1s infinite; }
  @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }

  .market-snapshot {
    display: grid; grid-template-columns: repeat(3,1fr); gap: 8px;
    margin-top: 12px; max-width: 560px;
    animation: fadeIn .3s ease;
  }
  @keyframes fadeIn { from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:none} }
  .snap-card { background: var(--surface2); border: 1px solid var(--border2); border-radius: var(--r-tile); padding: 10px 12px; }
  .snap-label { font-size: 11px; color: var(--muted); margin-bottom: 5px; }
  .snap-val { font-size: 17px; font-family: var(--font); }
  .snap-val.green { color: var(--green); }
  .snap-source { font-size: 11px; color: var(--dim); margin-top: 3px; }
  .snap-error { grid-column: 1/-1; font-size: 12px; color: var(--red); padding: 8px 12px; background: var(--red-bg); border-radius: var(--radius); border: 1px solid rgba(231,76,60,.2); }

  .lookup-hint { font-size: 11px; color: var(--muted); margin-top: 6px; font-style: italic; }

  .bundle-box {
    background: rgba(240,165,0,0.07); border: 1px solid var(--amber-border);
    border-radius: var(--radius); padding: 10px 12px; margin-bottom: 8px;
  }
  .bundle-box-title { font-size: 11px; color: var(--amber); margin-bottom: 6px; font-weight: 500; }
  .bundle-filler { font-size: 11px; color: var(--muted); padding: 2px 0; }
  .bundle-effective { font-size: 13px; color: var(--amber); margin-top: 6px; font-weight: 500; }

  /* ── Watched cards page ── */
  .watches-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
  /* Luke, 2026-10-26: "the .wc2-card can work with a min-width of 328px
     so we don't need to go down to one column until about 680px wide" —
     was 900px, dropping to single column much earlier than the card
     itself actually needs to. */
  @media (max-width: 680px) { .watches-grid { grid-template-columns: 1fr; } }

  /* ── Wins page ── */
  .win-stats { display:grid; grid-template-columns:repeat(4,1fr); gap:8px; margin-bottom:20px; }
  @media (max-width:800px) { .win-stats { grid-template-columns:repeat(2,1fr); } }
  .win-stat { background:var(--surface); border:1px solid var(--border); border-radius:var(--r-tile); padding:12px; }
  .win-stat-l { font-size: 11px; color:var(--muted); letter-spacing:.05em; text-transform:uppercase; margin-bottom:4px; }
  .win-stat-v { font-size:20px; font-weight:600; color:var(--text); }
  .win-stat-v.green { color:#45c77c; }

  .win-eyebrow { display:flex; align-items:center; gap:6px; margin-bottom:8px; }
  /* page_section_title's padding-top:10px is meant to separate a
     standalone section header from whatever precedes it — inside this
     inline icon+text row it just makes the flame and the text
     mis-align (the text's box grows taller, so flex's align-items:
     center throws them off-centre relative to each other). */
  .win-eyebrow .page_section_title { padding-top: 0 !important; }
  .win-eyebrow svg { color:var(--best); }

  /* Claim Wins (2026-09-01/02) — Lost alerts with a claimable win, plus
     clicked live listings from Watch setup, reusing the exact alert-item
     card (alertHTML/listingClickHTML in dashboard.js) for visual
     consistency with the Alerts page. Header is a .win-divider (same
     line-text-line, truly-centered shape as "Win History"/"All
     Purchases" below it — Luke, 2026-09-02: "the trophy claim wins
     title in the center, not left aligned") rather than the plain
     left-aligned .win-eyebrow other section labels on this page use. */
  .claim-wins-divider .page_section_title { display:flex; align-items:center; gap:6px; padding-top:0 !important; white-space:nowrap; }
  .claim-wins-icon { display:inline-flex; color:var(--best); }
  .claim-wins-count { font-size:11px; font-weight:600; color:var(--muted); background:var(--surface2); border:1px solid var(--border2); border-radius:20px; padding:1px 8px; }
  .claim-wins-list { margin-bottom:14px; }
  /* "Seen during setup" pill — the one deliberate visual difference on
     a listing-click card vs a real alert (no Buy/Negotiate verdict to
     show, since it was never matched/alerted on). Neutral, not
     green/amber, since it isn't a buy-signal the way those are. */
  /* Borderless, same as .alert-chip.buy/.negotiate — matches the pill
     convention already established elsewhere (fill + color only). */
  .alert-chip.seen { background:var(--surface2); color:var(--muted); }
  .win-eyebrow span { font-size:11px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--best); }

  .win-card { background:var(--surface); border:1px solid var(--border); border-radius:var(--r-nested); padding:12px; margin-bottom:10px; box-shadow:var(--shadow-sm); }
  .win-card.hero { border:2px solid var(--best); border-radius:var(--r-card); padding:14px; margin-bottom:20px; }
  :root[data-theme="light"] .win-card.hero { border-color: rgba(240,165,0,.75); }
  .win-card:last-child { margin-bottom:0; }

  .win-top { display:flex; gap:12px; }
  .win-thumb { height:100px; aspect-ratio:5/7; border-radius:4px; border:1px solid var(--border); background:var(--surface2); display:flex; align-items:center; justify-content:center; flex-shrink:0; overflow:hidden; }
  .win-thumb img { width:100%; height:100%; object-fit:cover; object-position:top; }
  .win-thumb.hero { height:120px; border-radius:5px; border:2px solid var(--best); animation: win-thumb-hero-glow 3.2s ease-in-out infinite; }
  @keyframes win-thumb-hero-glow {
    0%,100% { box-shadow:0 0 6px 1px rgba(240,165,0,.35), 0 0 14px rgba(240,165,0,.18); }
    50%      { box-shadow:0 0 11px 3px rgba(240,165,0,.7), 0 0 24px rgba(240,165,0,.4); }
  }

  .win-info { flex:1; min-width:0; }
  .win-name-row { display:flex; align-items:flex-start; justify-content:space-between; gap:8px; }
  .win-name-wrap { display:flex; align-items:center; gap:6px; min-width:0; flex-wrap:wrap; }
  .win-name { font-size:15px; font-weight:500; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  /* card_title/card_prices use !important, which would otherwise flatten
     the hero (best-deal) card's intentional size bump — re-assert it with
     matching specificity+importance so the hero variant still stands out. */
  .win-card.hero .win-name.card_title { font-size:16px !important; }
  .win-badge-best { display:inline-flex; align-items:center; gap:4px; font-size:11px; color:var(--best); border:1px solid var(--best-border); border-radius:var(--r-pill); padding:3px 9px; white-space:nowrap; flex-shrink:0; }
  .win-badge-best svg { width:11px; height:11px; flex-shrink:0; }
  .win-set { font-size:12px; color:var(--text-secondary); margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .win-seller { font-size:11px; color:var(--muted); margin-top:5px; display:flex; align-items:center; gap:4px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

  .win-saving { display:flex; align-items:baseline; gap:7px; margin-top:8px; }
  .win-card.hero .win-saving { margin-top:10px; }
  .win-saving-l { font-size:11px; font-weight:500; letter-spacing:.05em; color:var(--muted); }
  .win-saving-v { font-size:19px; font-weight:600; color:#45c77c; line-height:1; }
  .win-card.hero .win-saving-v.card_prices { font-size:24px !important; }
  .win-saving-p { font-size:13px; color:#45c77c; }
  .win-saving.over .win-saving-l, .win-saving.over .win-saving-v, .win-saving.over .win-saving-p { color:var(--red); }

  .win-barwrap { margin-top:10px; }
  .win-card.hero .win-barwrap { margin-top:12px; }
  .win-bar { height:8px; border-radius:6px; background:var(--surface2); overflow:hidden; display:flex; }
  .win-bar .paid { background:var(--surface3); }
  .win-bar .save { background:#45c77c; }
  .win-bar .over { background:var(--red); }
  .win-bar-labels { display:flex; justify-content:space-between; align-items:baseline; margin-top:7px; font-size:11px; }
  .win-bar-labels .l { color:var(--muted); }
  .win-bar-labels .l b { font-weight:500; color:var(--text); }
  .win-bar-labels .r { color:var(--muted); }

  .win-divider { display:flex; align-items:center; gap:10px; margin-bottom:14px; }
  .win-divider .line { flex:1; height:1px; background:var(--border); }
  .win-divider span { font-size:14px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); white-space:nowrap; }

  .win-month { display:flex; align-items:baseline; justify-content:space-between; margin-bottom:10px; }
  .win-month .m { font-size:14px; font-weight:700; color:var(--text); }
  .win-month .t { font-size:12px; color:var(--muted); }
  .win-month .t b { color:var(--green); font-weight:500; }
  /* Collapsible month groups on the Wins page (Luke, 2026-09-21) */
  .win-month { cursor:pointer; user-select:none; }
  .win-month .m { display:inline-flex; align-items:center; gap:6px; }
  .win-month-chev { flex-shrink:0; color:var(--muted); transition:transform .15s; }
  .win-month:hover .m, .win-month:focus-visible .m { color:var(--brand); }
  .win-month:hover .win-month-chev { color:var(--brand); }
  .win-month-group.collapsed .win-month-chev { transform:rotate(-90deg); }
  .win-month-group.collapsed .win-month-body { display:none; }

  /* ── Wallet & Shop ── */
  .wallet-tiles { display:grid; grid-template-columns:repeat(3,1fr); gap:10px; margin-bottom:20px; }
  .wallet-tile { background:var(--surface); border:1px solid var(--border); border-radius:var(--r-card); padding:14px; }
  .wallet-tile-head { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; margin-bottom:8px; }
  /* Luke, 2026-09-15: at the 360px minimum page width the Boosts tile's
     header row (icon + "Boosts" label) was overflowing its 1/3-width
     grid column and pushing the tile off the page — moving the icon
     down next to the value (a plain-text label alone fits easily) and
     sizing it up from 16 to 24 fixes both the overflow and gives the
     number more visual weight. */
  .wallet-tile-val { font-size:22px; font-weight:600; color:var(--text); display:flex; align-items:center; gap:4px; }
  .wallet-tile-val .ic { display:flex; flex-shrink:0; }
  .wallet-tile-val .ic svg { width:24px; height:24px; }
  .wallet-tile.boosts { min-width:115px; padding:14px 0px 14px 14px; }
  /* The "+N" tier-quota portion of the Boosts tile uses --boost (blue) to
     visually separate it from the plain owned-boosts count next to it —
     it's a recharging allowance, not a permanent balance, so it reads
     differently on purpose. */
  .wallet-boost-plus { color: var(--boost); font-weight: 600; }
  .boosts .wallet-tile-val .wallet-boost-plus { font-size: 18px; }

  .wallet-sub-card { background:var(--surface); border:1px solid var(--border); border-radius:var(--r-card); padding:16px; margin-bottom:14px; }
  .wallet-sub-row { margin-bottom:14px; }
  .wallet-sub-row:last-child { margin-bottom:0; }
  .wallet-card-title { font-size:13px; font-weight:600; color:var(--text); margin-bottom:12px; }
  .wallet-sub-label { display:flex; justify-content:space-between; font-size:12px; color:var(--text-secondary); margin-bottom:6px; }
  .wallet-sub-label b { color:var(--text); font-weight:600; }
  .wallet-bar { position:relative; height:8px; border-radius:6px; background:var(--surface2); overflow:hidden; }
  .wallet-bar .fill { height:100%; background:var(--brand); border-radius:6px; }
  /* Unit-boundary tick marks (watch slots bar) — deliberately just thin
     notches, not per-slot icons or 1-20 labels, since those wouldn't fit on
     mobile at a 20-slot cap. --surface (not a border/muted color) so they
     read as a "cut" through the fill as well as the empty track. */
  .wallet-bar-tick { position:absolute; top:0; bottom:0; width:1px; background:var(--surface); z-index:1; pointer-events:none; }
  /* Each bolt is absolutely positioned at its own segment's right-hand edge
     (see _boostBoltsHtml) so the Nth bolt lines up exactly with where the
     bar's fill reaches once N units are used — not flex-distributed, which
     put a bolt directly on the bar's "0" mark instead. */
  .wallet-boost-bolts { position:relative; height:20px; margin-bottom:10px; }
  .wallet-boost-bolt { position:absolute; top:0; transform:translateX(-50%); display:flex; color:var(--dim); }
  .wallet-boost-bolt.avail { color:var(--boost); }
  .wallet-boost-bolt svg { width:17px; height:17px; }
  @media (min-width:801px) {
    .wallet-boost-bolt svg { width:20px; height:20px; }
  }
  .wallet-recharge-note { font-size:11px; color:var(--muted); }


  /* Shop's own tab strip — deliberately not .aw-mode-toggle/.aw-mode-btn
     (shared with the Add Watch search/manual picker elsewhere): Luke,
     reviewing the muted grey-on-grey active state that component gives
     you, "make it more obvious this is a tab... better emphasize the
     selected option." Each tab fills solid with its own asset's accent
     color when active (red for Credits/Watches — Watches reuses the
     Watch Token accent, blue/purple for Boosts, matching the topup
     modal's own coloring) rather than one shared neutral highlight, so
     the active tab reads as clearly as the buttons inside it do. */
  .shop-tabs { display:flex; gap:6px; background:var(--surface); border:1px solid var(--border2); border-radius:var(--radius); padding:4px; margin-bottom:14px; }
  .shop-tab-btn { flex:1; display:flex; align-items:center; justify-content:center; gap:6px; padding:9px 0; border-radius:calc(var(--radius) - 2px); border:none; background:none; cursor:pointer; font-family:inherit; font-size:12.5px; font-weight:600; color:var(--muted); transition:background-color .15s, color .15s; }
  .shop-tab-btn .ic { display:flex; }
  .shop-tab-btn .ic svg { width:15px; height:15px; }
  .shop-tab-btn.credits .ic svg { outline:2px solid var(--surface); border-radius:20px; outline-offset:-1px; }
  .shop-tab-btn:hover:not(.active) { color:var(--text); }
  .shop-tab-btn.credits.active { background:var(--brand); color:#fff; }
  .shop-tab-btn.token.active { background:var(--brand); color:#fff; }
  .shop-tab-btn.boost.active { background:var(--boost); color:#fff; }

  /* Tier discount — Luke, 2026-09-12: was a full-width banner captioned
     "all Credits", but the discount applies to every financial item in
     the shop (credits, boosts, watch tokens), not just credits, and it
     was scoped inside the Credits tab's own body so it vanished on the
     other two tabs. Moved next to the page heading itself, where it's
     visible regardless of tab — brief pill, bold red percentage, small
     white "{tier} tier perk" label. Only rendered when there's an
     actual discount to show (free tier has nothing to advertise). */
  .shop-heading-discount { display:inline-flex; align-items:center; gap:6px; background:var(--brand-bg); border:1px solid var(--brand-border); border-radius:999px; padding:4px 12px; margin-left:2px; font-size:11px; font-weight:600; }
  .shop-heading-discount b { color:var(--brand); font-size:13px; font-weight:800; letter-spacing:-.01em; }
  .shop-heading-discount i { color:var(--text); font-style:normal; }

  /* Explainer card above each tab's purchase options (Luke, 2026-09-15:
     "a description that tells a user what the items in the shop do").
     Boosts keep their own purple accent; Credits/Watch Tokens used to
     share the brand-red tint too, but Luke (2026-09-18) said it "looked
     like a red error message" — switched to the neutral dark
     --shop-info-neutral-* tokens (see :root) instead, which needs its
     own text colours here since (unlike the red tint) it isn't light
     enough for the page's normal --text-secondary body colour. */
  .shop-info-card { text-align:center; border-radius:var(--r-nested); padding:14px 20px; margin-bottom:14px; }
  .shop-info-card.credits, .shop-info-card.token { background:var(--shop-info-neutral-bg); border:1px solid var(--shop-info-neutral-border); }
  .shop-info-card.boost { background:var(--boost-bg); border:1px solid var(--boost-border); }
  .shop-info-card-title { font-size:14px; font-weight:700; margin-bottom:4px; }
  .shop-info-card.credits .shop-info-card-title, .shop-info-card.token .shop-info-card-title { color:var(--shop-info-neutral-text); }
  .shop-info-card.boost .shop-info-card-title { color:var(--boost); }
  .shop-info-card-body { font-size:12.5px; line-height:1.5; color:var(--text-secondary); max-width:520px; margin:0 auto; }
  .shop-info-card.credits .shop-info-card-body, .shop-info-card.token .shop-info-card-body { color:var(--shop-info-neutral-text-secondary); }

  /* One row on desktop (plenty of room for icon+title+price AND the two
     actions) — only drops to two stacked lines at the mobile breakpoint,
     where the discounted-price text alone was already tight (Luke:
     "the Credit price doesn't need to be on a separate line when on
     desktop... just take the buttons down a line on mobile"). */
  .shop-bundle-row { display:flex; flex-direction:row; align-items:center; gap:12px; background:var(--surface); border:1px solid var(--border); border-radius:var(--r-nested); padding:12px 14px; margin-bottom:8px; }
  .shop-bundle-row-top { display:flex; align-items:center; gap:12px; flex:1; min-width:0; }
  .shop-bundle-icon { display:flex; align-items:center; justify-content:center; width:34px; height:34px; border-radius:50%; background:var(--surface2); color:var(--muted); flex-shrink:0; }
  /* Boosts/Watches bundle icons tinted to match the Buy Boosts/Buy Watch
     Token modal's own accent (topup-bundle-row.boost/.token) — Luke:
     "Watches shop bundle icons should have a red to them... Boosts shop
     bundle icons should have a blue to them (like in the buy
     boosts/watches modal)." Credits gets the same brand fill its Buy Now
     button already uses, rather than the plain neutral circle. */
  .shop-bundle-icon.boost { background:var(--boost); color:#fff; }
  .shop-bundle-icon.token { background:var(--brand); color:#fff; }
  /* The Credits coin icon is already its own multi-color badge (see
     WALLET_CREDIT_ICON) — a flat fill behind it doubles up on circles,
     so it gets the same thin brand ring the topup modal's own Credits
     row uses instead of Boosts/Watches' solid tint. */
  .shop-bundle-icon.credits { background:none; }
  /* Same halo the Buy Credits topup modal's own Credits row uses
     (.topup-bundle-row.credits .topup-bundle-icon svg) — Luke, 2026-09-15:
     "on out credits purchasing modals we have a slight halo/circle behind
     the credits icon... can we copy that?" The halo only reads as a
     "cutout badge" because its colour CONTRASTS with the row behind it
     (modal row is var(--brand-bg), halo is var(--bg) — visibly darker/
     lighter). First attempt used var(--surface) here to "match this row's
     background" — wrong instinct: .shop-bundle-row's own background IS
     var(--surface), so that halo colour matched its row exactly and just
     vanished (Luke: "that looks bad" — confirmed live, no visible cutout,
     just a thin ring floating in dead space). var(--bg) — the page
     background behind the row, not the row's own — is what actually
     contrasts here, same trick the modal uses one level up. */
  .shop-bundle-icon.credits svg { outline:solid 9px var(--bg); outline-offset:-1px; border:solid 1px var(--brand); border-radius:30px; }
  .shop-bundle-info { flex:1; min-width:0; }
  .shop-bundle-sub { font-size:11px; color:var(--muted); margin-top:2px; }
  .shop-bundle-price { text-align:right; flex-shrink:0; }
  .shop-bundle-price .face { font-size:11px; color:var(--dim); text-decoration:line-through; margin-right:4px; }
  .shop-bundle-price .now { font-size:14px; font-weight:600; color:var(--text); }
  /* Both actions size to their own content and sit together on the
     right — a flex:1 Buy button read fine in isolation but turned into
     a genuinely oversized button once there was a full row to fill.
     Luke: "Just allow both buy buttons to range right." */
  .shop-bundle-actions { display:flex; justify-content:flex-end; gap:8px; flex-shrink:0; }
  .shop-bundle-actions .shop-buy-btn { padding:7px 20px; }
  @media (max-width:480px) {
    .shop-bundle-row { flex-direction:column; align-items:stretch; gap:10px; }
  }
  .shop-buy-btn { background:var(--brand); color:#fff; border:none; border-radius:var(--radius); padding:7px 16px; font-size:12px; font-weight:700; cursor:pointer; font-family:inherit; flex-shrink:0; }
  .shop-buy-btn:disabled { background:var(--surface2); color:var(--dim); cursor:not-allowed; }
  .shop-stepper { display:flex; align-items:center; gap:6px; flex-shrink:0; }
  .shop-stepper button { width:26px; height:26px; border-radius:50%; border:1px solid var(--border2); background:var(--surface2); color:var(--text); font-size:14px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-family:inherit; }
  .shop-stepper .n { min-width:18px; text-align:center; font-size:13px; font-weight:600; }
  /* Physical Merch / Competitions catalog — max 9 products / 6 draws,
     no pagination, so this is a plain grid, not an infinite-scroll list.
     Card visuals lead with the photo; title/description/price sit below
     with a quick action. Clicking the image expands that card in place
     (see .shop-exp-*) — only one product and one draw can be expanded
     at once, each independently, same as admin's own Shop Manager. */
  /* Section headers use the app's standard .page_title sizing (same
     treatment as the "SHOP" heading itself) rather than a bespoke size —
     Luke: "stick to our standard and typical font sizes and classes."
     scroll-margin-top clears the sticky .wallet-strip (see the same
     value on .shop-prod-card.expanded) so the #merch/#competitions
     anchors land correctly instead of landing underneath it. */
  .shop-catalog-heading { display:flex; align-items:center; gap:12px; margin:36px 0 14px; scroll-margin-top:73px; }
  .shop-catalog-heading:first-of-type { margin-top:8px; }
  .shop-catalog-heading svg { color:var(--brand); flex-shrink:0; }
  .shop-catalog-heading h3 { margin:0; }
  .shop-catalog-heading h3 a { color:var(--text); text-decoration:none; }
  .shop-catalog-heading h3 a:hover { color:var(--brand); }
  .shop-catalog-heading .line { flex:1; height:1px; background:var(--border2); }
  .shop-catalog-empty { font-size:12.5px; color:var(--muted); padding:10px 0 4px; }

  /* auto-fit + a 280px floor, capped at 3 columns — a card never gets
     narrower than 280px, and never more than 3 sit in a row even on a
     wide screen. Each column's minimum is whichever is larger: 280px,
     or a third of the row (so on a wide screen the "1/3 of row" side
     wins and forces the 4th card to wrap instead of squeezing in);
     column count still falls out of the fit rather than being guessed
     per viewport width below that. Applies at every width, matching
     .shop-prod-card's own min-width below — Luke, 2026-09-14: "on
     mobile... we are still going smaller than 280px min-width" (this
     used to only raise the floor on desktop, leaving mobile at the old
     250px). .pane's own 14-16px side padding leaves comfortable room
     for 280px even on a 320px-wide phone. */
  /* auto-fill, not auto-fit: with fewer items than fit a row, auto-fit
     collapses the leftover empty track(s) and hands their width to the
     populated 1fr tracks — but .shop-prod-card below has a FIXED width
     (var(--shop-prod-card-w), not stretch), so that reclaimed space just
     shows up as dead space between cards instead of packing them left
     (real bug, caught live via screenshot, Luke, 2026-09-15: 2 Merch
     items spread across a 3-column-wide row instead of sitting together
     on the left). auto-fill keeps the empty track's width share instead
     of collapsing it, so cards pack left and any leftover space lands
     after the last item, not between items. */
  .shop-catalog-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(max(280px, (100% - 32px) / 3), 1fr)); gap:16px; align-items:start; }
  /* Luke, 2026-09-18: the loading spinner (.pane-loading, the grid's only
     child while stock is still loading) was landing in just the grid's
     first auto-fill column — narrow, and pinned left — rather than
     centered across the page like the Credits/Watches/Boosts spinner.
     Span it across every column so its own justify-content:center
     actually centers against the full grid width. */
  .shop-catalog-grid .pane-loading { grid-column: 1 / -1; }

  .shop-prod-card { background:var(--surface); border:1px solid var(--border2); border-radius:var(--r-card); overflow:hidden; display:flex; flex-direction:column; box-shadow:var(--shadow-sm); transition:box-shadow .15s, transform .15s; }
  .shop-prod-card:not(.expanded):hover { box-shadow:var(--shadow-md); transform:translateY(-2px); }
  /* Expanded card spans the full row — the grid's own auto row-sizing
     handles pushing later items down, no JS reflow needed. scroll-margin-
     top keeps it clear of the sticky wallet strip (.wallet-strip, 61px)
     when JS scrolls it into view on expand — the actual <header> above
     that isn't sticky, so once scrolled the wallet strip is the only
     thing still pinned at the top. */
  .shop-prod-card.expanded { grid-column:1 / -1; scroll-margin-top:73px; }

  .shop-prod-media { position:relative; aspect-ratio:1; background:var(--surface2); cursor:pointer; overflow:hidden; }
  .shop-prod-media .strip { display:flex; width:100%; height:100%; transition:transform .3s cubic-bezier(.4,0,.2,1); }
  .shop-prod-slide { flex:0 0 100%; display:flex; align-items:center; justify-content:center; }
  /* Luke, 2026-09-14: a collapsed card (and its image slide, which
     otherwise just inherits whatever width the auto-fit grid hands its
     track) shouldn't grow past a fixed width just because there happen
     to be few products in the shop — capped identically on both
     selectors so they can never drift out of sync. Explicitly NOT
     applied to .expanded (Luke: "unless the product/competition is
     expanded") — that variant is a full-row two-column layout
     (.shop-exp-wrap) that deliberately uses the whole row width.

     Merch and Competitions are two separate grids (#shop-merch-grid,
     #shop-comp-grid), each auto-fitting its OWN item count — so a
     max-width alone still lets them drift apart (Merch's single item
     stretching wide while 5 Competitions squeeze
     narrower). --shop-prod-card-w is computed by
     syncShopProdCardWidths() in dashboard.js from whichever section is
     actually narrower and shared across both — "smallest width takes
     preference" (Luke). Falls back to 100% (plain track-stretch, capped
     by max-width above) until that JS has run once.

     Originally desktop-only (min-width:801px); Luke, 2026-09-14: "on
     mobile... we are still going smaller than 280px min-width. and we
     are not in step with consistent... size of the cards/tiles" — both
     the min-width and the cross-section sync apply at every width now,
     same as .shop-catalog-grid's floor right above.

     max-width is desktop-only again as of the same day's last ask —
     "Desktop view min-width:280 max-width:430 / Mobile view (below
     801px) min-width:280 max-width:none" — mobile cards are only ever
     floored at 280px, never capped, so a single item on a phone still
     fills the available width edge-to-edge. */
  .shop-prod-card:not(.expanded) { min-width:280px; width:var(--shop-prod-card-w, 100%); }
  .shop-prod-card:not(.expanded) .shop-prod-slide { min-width:280px; }
  @media (min-width:801px) {
    .shop-prod-card:not(.expanded) { max-width:430px; }
    .shop-prod-card:not(.expanded) .shop-prod-slide { max-width:430px; }
  }
  .shop-prod-slide img { width:100%; height:100%; object-fit:cover; }
  .shop-prod-slide .ph { color:var(--dim); width:30%; height:30%; }
  .shop-prod-arrow { position:absolute; top:50%; transform:translateY(-50%); width:26px; height:26px; border-radius:50%; background:rgba(255,255,255,.85); color:#111; border:none; display:flex; align-items:center; justify-content:center; cursor:pointer; opacity:0; transition:opacity .15s; z-index:2; }
  .shop-prod-media:hover .shop-prod-arrow { opacity:1; }
  .shop-prod-arrow.prev { left:8px; } .shop-prod-arrow.next { right:8px; }
  .shop-prod-dots { position:absolute; bottom:8px; left:0; right:0; display:flex; justify-content:center; gap:4px; z-index:2; }
  .shop-prod-dot { width:5px; height:5px; border-radius:50%; background:rgba(255,255,255,.55); transition:background .15s, width .15s; }
  .shop-prod-dot.active { background:#fff; width:14px; border-radius:3px; }
  .shop-prod-status { position:absolute; top:9px; left:9px; z-index:2; display:flex; align-items:center; gap:5px; background:rgba(0,0,0,.55); color:#fff; font-size:12px; font-weight:600; padding:3px 8px 3px 6px; border-radius:var(--r-pill); }
  .shop-prod-status .d { width:6px; height:6px; border-radius:50%; }
  /* Instagram giveaways: badge on the thumbnail, opposite the status pill (Luke, 2026-09-20). */
  .shop-prod-igbadge { position:absolute; top:9px; right:9px; z-index:2; width:28px; height:28px; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,.55); color:#fff; border-radius:50%; }
  .shop-prod-igbadge svg { width:16px; height:16px; }
  /* ── Skeleton loading (Luke, 2026-09-21) ─────────────────────────────
     Grey silhouettes of the real content with a soft diagonal shimmer sweeping left to right, shown
     until the data arrives. Colours are mixed from the theme's own text/surface tokens, so the same
     rules work in light and dark. They only appear after 150ms (a fast response never flashes them)
     and stand still when the visitor prefers reduced motion. */
  @keyframes sk-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
  @keyframes sk-in { to { opacity: 1; } }
  @keyframes sk-reveal { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
  .sk-reveal { animation: sk-reveal .5s ease-out; }
  @media (prefers-reduced-motion: reduce) { .sk-reveal { animation: none; } }
  .sk, .sk-mask {
    background-color: color-mix(in srgb, var(--text) 8%, var(--surface));
    background-image: linear-gradient(105deg, transparent 32%, color-mix(in srgb, var(--text) 10%, transparent) 50%, transparent 68%);
    background-size: 200% 100%; background-repeat: no-repeat;
    border-radius: 6px;
  }
  .sk { display: block; flex-shrink: 0; opacity: 0; animation: sk-in 0s linear .15s forwards, sk-shimmer 1.5s linear infinite; }
  .sk-mask { color: transparent !important; user-select: none; pointer-events: none; display: inline-block; min-width: 3.2ch; border-radius: 6px; animation: sk-shimmer 1.5s linear infinite; }
  .sk-mask * { color: transparent !important; }
  .sk-host { pointer-events: none; user-select: none; }
  #ref-code-value.sk-mask { min-width: 9ch; }
  .ref-code-actions .sk-mask { flex: 1; }
  .sk-stack { display: flex; flex-direction: column; gap: 8px; min-width: 0; flex: 1; }
  .sk-line { height: 12px; }
  .sk-pill { border-radius: 999px; }
  .sk-circle { border-radius: 50%; }
  .sk-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  @media (prefers-reduced-motion: reduce) {
    .sk { animation: sk-in 0s linear .15s forwards; background-image: none; }
    .sk-mask { animation: none; background-image: none; }
  }

  /* Modal preview mode (admin Modals viewer): show only the real overlay(s) on a plain page. */
  html.modal-preview body { background: var(--bg); min-height: 100vh; }
  html.modal-preview body > *:not(.overlay):not(#mp-stage):not(#mq-badge):not(script):not(style):not(link) { display: none !important; }
  html.modal-preview #mp-stage { pointer-events: none; }
  /* Startup modal queue: "1 of X" pill above the active modal, and stacked small notices (Luke, 2026-09-21). */
  .mq-badge { position:fixed; top:16px; left:50%; transform:translateX(-50%); z-index:100000; padding:4px 12px; border-radius:var(--r-pill); background:var(--surface); border:1px solid var(--border2); color:var(--text); font-size:12px; font-weight:600; box-shadow:var(--shadow-sm); opacity:0; pointer-events:none; transition:opacity .15s; }
  .mq-badge.show { opacity:1; }
  .mq-stack-item + .mq-stack-item { margin-top:12px; padding-top:12px; border-top:1px solid var(--border2); }

  .shop-prod-body { padding:11px 12px 12px; display:flex; flex-direction:column; gap:6px; flex:1; }
  /* Typography for title/description/price comes from the standard
     .card_title/.card_sub_title/.value_emphasis classes (applied
     alongside these in dashboard.js) — these rules are layout/colour
     only now, not font-size, per "stick to our standard and typical
     font sizes and classes." */
  .shop-prod-title { line-height:1.3; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .shop-prod-desc { color:var(--muted); line-height:1.45; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; flex:1; }
  .shop-prod-meta { display:flex; align-items:center; justify-content:space-between; gap:8px; font-size:12px; color:var(--muted); }
  .shop-prod-price { color:var(--text); }
  .shop-prod-price .from { font-size:12px; font-weight:500; color:var(--muted); margin-right:3px; }
  /* Tier-discounted Merch price — same .face (struck-through original) /
     .now treatment as the Credits bundle picker's .shop-bundle-price,
     applied wherever a Merch price renders (compact card + expanded). */
  .shop-prod-price .face, .shop-exp-price .face { font-size:0.85em; color:var(--dim); text-decoration:line-through; margin-right:4px; font-weight:400; }

  /* Size/colour pills — replace the old "N sizes" text in the price row
     (Luke: "if the size pills replace that text... we keep the rows
     consistent") rather than adding a whole extra row for them. Small
     on the card face, a touch bigger in the expanded view
     (.shop-exp-variant already exists for that). */
  .shop-card-variant-row { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:4px; }
  .shop-card-variant { border:1px solid var(--border2); border-radius:var(--r-item); padding:2px 7px; font-size:12px; font-weight:600; cursor:pointer; background:var(--surface); color:var(--text); font-family:inherit; line-height:1.4; }
  .shop-card-variant.sel { border-color:var(--brand); color:var(--brand); background:var(--brand-bg); }

  /* Action row — the basket icon is the only "add to basket" control
     (Luke: "SIMPLE shop that will never have LOTS of products", no
     separate labelled button needed); Buy Now is the quick-buy path,
     bypassing the basket entirely. */
  .shop-prod-action-row { display:flex; gap:8px; margin-top:2px; }
  .shop-exp-info .shop-prod-action-row { margin-top:14px; }
  .shop-prod-action-row .shop-buy-btn { flex:1; display:flex; align-items:center; justify-content:center; gap:6px; }
  /* Secondary action — a lighter shade of the Buy Now red (brand-bg/
     brand-border, not the solid brand fill) so the hierarchy between
     "add for later" and "buy this now" reads at a glance. Disabled
     drops all colour (grey, not red) so "can't act yet" is obvious
     rather than just a slightly duller red. */
  .shop-basket-add-btn { flex-shrink:0; display:flex; align-items:center; justify-content:center; gap:6px; padding:0 12px; border-radius:var(--radius); border:1px solid var(--brand-border); background:var(--brand-bg); color:var(--brand); font-family:inherit; font-size:12px; font-weight:650; white-space:nowrap; cursor:pointer; }
  .shop-basket-add-btn svg { flex-shrink:0; }
  .shop-basket-add-btn:hover:not(:disabled) { background:var(--brand-border); }
  .shop-basket-add-btn:disabled { background:var(--surface2); border-color:transparent; color:var(--dim); cursor:not-allowed; }

  /* Expanded card — image and full detail side by side, in place of the
     compact face, in the same spot in the grid. Plain single-row grid
     by default (products: just media + info). Competitions add a third
     grid item, the sold/draws facts panel, via the .has-panel modifier
     below — a *different* grid template (two rows in the left column),
     not just an extra unused area, since a reserved-but-empty "panel"
     row still eats height even with nothing in it, leaving dead space
     under the image on cards that don't have one (products). */
  .shop-exp-wrap { display:grid; grid-template-columns:1fr 1.3fr; }
  .shop-exp-wrap.has-panel {
    grid-template-areas:
      "media info"
      "panel info";
  }
  @media (max-width:740px) {
    .shop-exp-wrap { grid-template-columns:1fr; }
    .shop-exp-wrap.has-panel {
      grid-template-areas:
        "media"
        "info"
        "panel";
    }
  }
  /* grid-area only means anything where grid-template-areas actually
     names "media"/"info"/"panel" (the .has-panel variant above) — on
     the plain wrap (products), an unmatched named grid-area confuses
     the browser into fabricating extra implicit tracks instead of just
     falling back to normal auto-placement, so these stay scoped. */
  .shop-exp-wrap.has-panel .shop-exp-media { grid-area:media; }
  .shop-exp-wrap.has-panel .shop-exp-info { grid-area:info; }
  .shop-exp-panel { grid-area:panel; }
  /* Desktop: no fixed aspect ratio — a square sized off the media
     column's own (often wide) width could end up far taller than the
     info column actually needs, leaving a slab of dead space beside a
     short description/single-prize draw. Grid's default stretch gives
     the media column the info column's own content height instead, and
     the slide <img> (already width/height:100% + object-fit:cover)
     covers whatever that height turns out to be. min-height is just a
     floor so a very short info column doesn't squash the photo to a
     sliver. Mobile keeps a fixed aspect ratio since the columns stack
     there and there's no "match the other column" to do. */
  /* aspect-ratio:auto cancels .shop-prod-media's own aspect-ratio:1
     (both classes apply together — see shopExpandedCompHtml/
     shopExpandedProductHtml's class="shop-exp-media shop-prod-media").
     Without it, the base square ratio was still winning here: once
     min-height:280px forced this box taller than the grid column's own
     (often narrower, e.g. with the Watches sidebar open) width, the
     still-active 1:1 ratio pushed the box's WIDTH out to match that
     280px height instead — overflowing past the actual grid column
     boundary and physically covering the first character or two of
     "COMPETITION"/"PRIZES"/the prize ranks in the info column beside
     it. Luke, screenshot: text clipped with the sidebar open. */
  .shop-exp-media { position:relative; background:var(--surface2); min-height:280px; min-width:0; aspect-ratio:auto; }
  @media (max-width:740px) { .shop-exp-media { aspect-ratio:16/9; min-height:0; } }
  .shop-exp-media .shop-prod-slide .ph { width:26%; height:26%; }
  .shop-exp-media .shop-prod-arrow { width:36px; height:36px; opacity:1; background:rgba(255,255,255,.9); }
  .shop-exp-media .shop-prod-arrow.prev { left:12px; } .shop-exp-media .shop-prod-arrow.next { right:12px; }
  .shop-exp-media .shop-prod-dots { bottom:12px; }
  .shop-exp-media .shop-prod-dot { width:6px; height:6px; } .shop-exp-media .shop-prod-dot.active { width:16px; }
  /* min-width:0 overrides a CSS Grid item's default min-width:auto —
     without it, the grid track refuses to shrink below its content's
     intrinsic width and just overflows/clips instead of wrapping once
     the page's own available width narrows (e.g. the Watches sidebar
     open on desktop). Luke, screenshot: "COMPETITION"/"PRIZES" text
     clipped on the left edge with the sidebar open. */
  .shop-exp-info { padding:20px 22px; display:flex; flex-direction:column; min-width:0; position:relative; }
  @media (max-width:740px) { .shop-exp-info { padding:16px 18px; } }
  /* Luke, 2026-09-14: ".shop-exp-kicker" (the plain "Merch"/
     "Competition" label) removed entirely — "can the .shop-exp-kicker
     disappear. no value." — and "Show less" moved off the info column
     into the media itself: "Can the 'Show less' appear inside the
     bottom right corner of the .shop-exp-media?" It's a real child of
     .shop-exp-media now (was a sibling in .shop-exp-info), overlaid
     the same way the carousel arrows/dots already are there.
     Mobile-only per Luke's same-day follow-up: on desktop the close
     affordance is the .shop-exp-close X in the info column instead
     (".On desktop rather than the show less button, we have an X to
     close the expanded view, which floats in the top right of the
     shop-exp-info"). */
  .shop-exp-collapse {
    position:absolute; bottom:10px; right:10px; z-index:2;
    background:rgba(255,255,255,.9); border:none; border-radius:var(--r-pill); padding:5px 12px;
    font-size:12px; font-weight:600; color:#111; cursor:pointer; display:flex; align-items:center; gap:5px;
    font-family:inherit; box-shadow:var(--shadow-sm);
  }
  .shop-exp-collapse:hover { background:#fff; }
  @media (min-width:741px) { .shop-exp-collapse { display:none; } }
  /* Desktop-only close X in the top-right of .shop-exp-info (Luke,
     2026-09-14, see note above) — hidden on mobile, where
     .shop-exp-collapse in the media corner does the same job. */
  .shop-exp-close {
    display:none; position:absolute; top:14px; right:14px; z-index:2;
    width:28px; height:28px; align-items:center; justify-content:center;
    border:none; border-radius:50%; background:var(--surface2); color:var(--text-secondary);
    cursor:pointer; padding:0;
  }
  .shop-exp-close:hover { background:var(--border2); color:var(--text); }
  @media (min-width:741px) { .shop-exp-close { display:flex; } }
  /* Typography comes from the standard .detail-title class (same class
     the Watch detail page uses for its title), overridden to 14px here
     specifically (Luke, 2026-09-14: "can .shop-exp-title .detail-title
     stay at 14px?" — was inheriting .detail-title's 22px). Mobile-only
     per Luke's same-day follow-up: "on desktop we don't need a
     font-size override... (only on mobile)" — desktop keeps
     .detail-title's default 22px. */
  @media (max-width:740px) { .shop-exp-title.detail-title { font-size:14px; } }
  .shop-exp-title { margin-bottom:10px; }
  .shop-exp-status { display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:600; padding:4px 10px; border-radius:var(--r-pill); width:fit-content; margin-bottom:12px; }
  .shop-exp-status .d { width:6px; height:6px; border-radius:50%; }
  .shop-exp-desc { font-size:13px; color:var(--text-secondary); line-height:1.6; margin-bottom:12px; }
  .shop-exp-variants { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:16px; }
  .shop-exp-variant { border:1px solid var(--border2); border-radius:var(--radius); padding:6px 12px; font-size:12px; font-weight:600; cursor:pointer; background:var(--surface); color:var(--text); font-family:inherit; }
  .shop-exp-variant.sel { border-color:var(--brand); color:var(--brand); background:var(--brand-bg); }
  .shop-exp-price-row { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:auto; padding-top:12px; }
  /* Typography comes from the standard .card_prices class — nothing
     shop-specific to add here. */
  .shop-exp-price .unit { font-size:12.5px; font-weight:600; color:var(--muted); }
  .shop-exp-info .shop-buy-btn { padding:10px 22px; font-size:12.5px; }

  /* Luke, 2026-09-14: "make these titles red" — "Prizes"/"Skill question". */
  .shop-exp-section-label { font-size:12px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; color:var(--red); margin-bottom:7px; }
  .shop-exp-prizes { list-style:none; margin:0 0 12px; padding:0; display:flex; flex-direction:column; gap:5px; }
  .shop-exp-prizes li { display:flex; align-items:baseline; gap:4px; font-size:12.5px; }
  /* Luke, 2026-09-14: "close the gap after the prize position" — the
     30px min-width reserved a fixed column even for a short "1st",
     leaving visible dead space before the prize title. */
  .shop-exp-prizes .rank { flex-shrink:0; color:var(--muted); font-weight:600; font-size:12px; }
  .shop-exp-prizes .t { color:var(--text); }
  .shop-exp-prizes-empty { font-size:12px; color:var(--muted); margin-bottom:16px; }

  .shop-exp-question { margin-bottom:12px; }
  .shop-exp-question-text { font-size:12.5px; color:var(--text); margin-bottom:8px; line-height:1.5; }
  /* Replaced the native <select> with real toggle buttons (Luke,
     2026-09-14: "for skill questions... can we have buttons instead?
     Toggles" — the select's own built-in chrome had been rendering
     noticeably taller/bolder than the rest of this panel regardless of
     font-size, a native-control quirk this sidesteps entirely). Wraps
     like .shop-exp-variant's pills rather than stacking full-width one
     per row — Luke, same day, seeing it stacked: "we need to make these
     buttons wrap... to minimise the consumed page real estate." */
  .shop-exp-answer-toggles { display:flex; flex-wrap:wrap; gap:6px; }
  .shop-exp-answer-btn {
    text-align:left; background:var(--surface); border:1px solid var(--border2); border-radius:var(--radius);
    padding:8px 12px; font-size:12.5px; color:var(--text); font-family:inherit; cursor:pointer;
  }
  .shop-exp-answer-btn:hover { border-color:var(--muted); }
  .shop-exp-answer-btn.sel { border-color:var(--brand); color:var(--brand); background:var(--brand-bg); font-weight:600; }

  /* Sold/draw facts panel — sits flush under the image, no border-box
     look; a single right-edge divider separates it from the info column
     on desktop instead. Grid-placed below the image (desktop) or below
     the rest of the info column (mobile), see .shop-exp-wrap's
     grid-template-areas above. The sold/allowance line spans the full
     panel width and splits left/right (mirroring the full-width bar
     below it); the draws note and per-person limit underneath just
     center, since there's nothing to anchor them to either side. */
  .shop-exp-panel { margin:0; padding:16px 18px; border-right:1px solid var(--border2); display:flex; flex-direction:column; align-items:center; text-align:center; gap:6px; }
  /* On mobile the panel drops below the info column instead of sitting
     beside it, so the divider moves from a right border to a top one
     (Luke, 2026-09-14: "On mobile... add a hairline top border to
     .shop-exp-panel"). */
  @media (max-width:740px) { .shop-exp-panel { border-right:none; border-top:1px solid var(--border2); } }
  .shop-exp-panel-fact { font-size:12px; color:var(--muted); }
  .shop-exp-panel-fact-row { display:flex; align-items:baseline; justify-content:space-between; gap:6px; width:100%; font-size:12px; color:var(--muted); }
  .shop-exp-progress { width:100%; }
  .shop-exp-progress .bar { width:100%; height:5px; border-radius:3px; background:var(--surface3); overflow:hidden; }
  .shop-exp-progress .fill { height:100%; background:var(--brand); border-radius:3px; transition:width .2s; }
  .shop-exp-draws-note { font-size:12px; color:var(--muted); }
  /* "Terms" appended as its own one-word sentence at the end of the
     description (Luke, 2026-10-08: "reduce the real estate given to the
     terms... as a one word sentence, as a brand coloured hyperlink") —
     replaces the earlier full "Draws Terms & Conditions" line that used
     to sit in the bottom panel. */
  .shop-exp-terms-link { color:var(--brand); font-weight:600; text-decoration:none; }
  .shop-exp-terms-link:hover { text-decoration:underline; }
  /* "You have N tickets for this draw" — Luke: "use the ticket icon
     too." Sits above the sold/max stats as its own callout, same
     brand-tinted-pill language as other personal-stat call-outs in the
     app (e.g. .reveal-row.you), not just plain muted text like the
     aggregate facts around it. */
  /* Plain right-aligned meta on the price row, opposite the ticket
     price — deliberately NOT a chip/pill (Luke: "don't put it in a red
     shape that makes it look like a warning"). Just muted text + icon. */
  .shop-exp-your-tickets { display:flex; align-items:center; gap:6px; font-size:12px; font-weight:600; color:var(--text-secondary); white-space:nowrap; text-decoration:none; }
  .shop-exp-your-tickets:hover { color:var(--brand); }
  .shop-exp-your-tickets:hover .ic { color:var(--brand); }
  .shop-exp-your-tickets .ic { display:flex; flex-shrink:0; color:var(--muted); }
  .shop-exp-your-tickets .ic svg { width:14px; height:14px; }
  /* Mirrors the merch product's own bottom pattern (.shop-exp-price-row
     pinned to the info column's bottom edge, then a dedicated action row
     below it — see shopExpandedProductHtml's .shop-exp-price-row +
     .shop-prod-action-row): price alone on its own line, then the qty
     stepper on the left and Buy Ticket(s) on the right, in the same
     positions merch's +Basket/Buy Now occupy. */
  /* No mobile stacking override — adopts merch's own .shop-prod-action-row
     principle of always staying one row: the stepper is fixed-content
     width and the button is flex:1, so the row just naturally
     shrinks/grows at any width instead of needing a breakpoint. */
  .shop-exp-comp-action-row { display:flex; align-items:center; gap:12px; margin-top:14px; }
  /* Grows to fill the space beside the stepper — same as merch's own
     .shop-prod-action-row .shop-buy-btn — rather than a fixed width. */
  .shop-exp-comp-action-row .shop-buy-btn { flex:1; text-align:center; }
  /* One bordered pill holding the whole stepper (icons + number
     together), not separate colour-coded icons floating outside a
     number-only box — the +/- are plain neutral glyphs, not red/green. */
  .shop-exp-qty-stepper { display:flex; align-items:center; border:1px solid var(--border2); border-radius:10px; background:var(--surface2); }
  .shop-exp-qty-stepper button { width:34px; height:34px; padding:0; border:none; background:none; font-size:17px; font-weight:600; color:var(--text); cursor:pointer; display:flex; align-items:center; justify-content:center; font-family:inherit; line-height:1; flex-shrink:0; }
  .shop-exp-qty-stepper button:disabled { color:var(--dim); cursor:not-allowed; }
  .shop-exp-qty-stepper .n { min-width:26px; text-align:center; font-size:14.5px; font-weight:650; color:var(--text); }

  /* Basket / Quick Buy modal line items. Every row — item lines and the
     Subtotal/Saving/Postage/Total rows below — shares the same fixed
     ROW_H height and zero vertical padding (content is centered by
     align-items instead), so the whole basket reads as one consistently
     ruled table — Luke: "our rows are all inconsistent heights" (a
     min-height plus differing padding per row-type wasn't actually
     enough to guarantee that; a shared fixed height is). */
  .shop-basket-line, .shop-basket-subtotal-row, .shop-basket-savings-row, .shop-basket-postage-row, .shop-basket-total-row {
    display:flex; align-items:center; justify-content:space-between; height:34px; border-bottom:1px solid var(--border);
  }
  /* Whichever row ends up last inside #shop-basket-lines (an item line
     when there's no discount/postage to show, a Postage row when there
     is) loses its own bottom border — Total's border-top a few pixels
     below is already the closing divider for this whole group, and two
     borders that close together read as cluttered/inconsistent
     (Luke: "work on the row borders and spacing" — sharpest in the
     plain Credits-only, no-discount case, where it's the only thing
     between the last line and Total). One clean line, not two. */
  #shop-basket-lines > *:last-child { border-bottom:none; }
  .shop-basket-line { gap:10px; }
  .shop-basket-line-info { flex:1; min-width:0; }
  /* Small, sized-to-content — not the bordered full-width pill used
     elsewhere (.shop-exp-qty-stepper), which read as oversized stretched
     across a basket line. Floors at 1: the only way below that is the
     trash icon, same as before. A fixed width (not just sized-to-content)
     so every line's stepper sits at the same X position regardless of
     how long that line's title is — Luke: "steppers should align (fixed
     position)," sitting just left of the price column. */
  .shop-basket-qty-stepper { display:inline-flex; align-items:center; justify-content:center; gap:6px; flex-shrink:0; width:70px; }
  .shop-basket-qty-stepper button { width:20px; height:20px; padding:0; border:1px solid var(--border2); border-radius:50%; background:var(--surface2); color:var(--text); font-size:12px; font-weight:600; cursor:pointer; display:flex; align-items:center; justify-content:center; font-family:inherit; line-height:1; flex-shrink:0; }
  .shop-basket-qty-stepper button:disabled { color:var(--dim); border-color:transparent; cursor:not-allowed; }
  .shop-basket-qty-stepper .n { min-width:14px; text-align:center; font-size:12px; font-weight:650; color:var(--text); }
  /* Every price in the basket shares this one weight — item lines,
     Subtotal, Saving, Postage — so nothing outshouts anything else
     except Total (Luke: "All prices should be the same font weight with
     exception of the final total"). */
  .shop-basket-line-price, .shop-basket-subtotal-row span:last-child, .shop-basket-savings-row span:last-child, .shop-basket-postage-row span:last-child {
    font-weight:600; flex-shrink:0; text-align:right;
  }
  .shop-basket-line-price { min-width:64px; }
  /* Pulled 4px closer to the price than the row's own 10px gap would
     put it — Luke: "tighten the gap between the trashcans and the
     values" — without touching the info/stepper/price spacing, which
     reads fine as-is. */
  .shop-basket-remove { flex-shrink:0; width:24px; height:24px; margin-left:-4px; border-radius:50%; border:none; background:none; color:var(--muted); font-size:16px; cursor:pointer; display:flex; align-items:center; justify-content:center; line-height:1; }
  .shop-basket-remove:hover { color:var(--red); }
  /* Right padding matches the item lines' trash-icon column exactly
     (24px button + 6px gap, after the tightening above = 30px) — Luke:
     "price positioning." Without it these values sit further right than
     the item prices above, since summary rows have no trash-icon column
     of their own eating into that space. */
  .shop-basket-subtotal-row, .shop-basket-savings-row, .shop-basket-postage-row, .shop-basket-total-row { padding-right:30px; }
  .shop-basket-subtotal-row, .shop-basket-postage-row { font-size:12px; color:var(--muted); }
  .shop-basket-savings-row { font-size:12px; color:var(--green); }
  .shop-basket-savings-row span:first-child { font-weight:600; }
  /* Total is the one row that's meant to stand out — Luke: "the grey
     highlight on the total line looks clunky" (a background fill was
     the wrong tool); a solid, heavier top rule plus a bigger bold label
     does the job instead, no fill. The amount already carries weight
     via .card_prices (700, same as every other price is now NOT). */
  .shop-basket-total-row { border-bottom:none; border-top:2px solid var(--border2); margin-top:2px; }
  .shop-basket-total-row .card_sub_title { font-size:14px !important; font-weight:700 !important; color:var(--text); }
  .shop-basket-buy-later { flex:1; background:none; border:1px solid var(--border2); border-radius:var(--radius); padding:11px; font-family:inherit; font-size:12px; font-weight:650; color:var(--text); cursor:pointer; }
  .shop-basket-buy-later:hover { border-color:var(--muted); }

  /* Standard site footer (same content/structure as home.css's
     .site-footer) — rebuilt with dashboard's own tokens (--surface2,
     --border2) rather than reusing home.css's --bg-raised/--border-
     strong, which dashboard.css doesn't define. */
  .site-footer { background:var(--bg); border-top:1px solid var(--border); padding:40px 0 28px; margin-top:36px; }
  .footer-inner { max-width:1200px; margin:0 auto; padding:0 16px; box-sizing:border-box; }
  .footer-inner { padding:0 32px; }
  @media (max-width:800px) { .footer-inner { padding:0 20px; } }
  /* Same logo + social sizing/positioning as the home page footer (Luke,
     2026-09-19): logo left, icons right on one row at every width. */
  .footer-top { display:flex; align-items:center; justify-content:space-between; flex-wrap:nowrap; gap:12px; padding-bottom:32px; border-bottom:1px solid var(--border); }
  .footer-logo { min-width:0; flex:0 1 auto; }
  .footer-logo img { display:block; height:60px; width:auto; max-width:100%; object-fit:contain; object-position:left center; position:relative; left:-14px; }
  .footer-socials { display:flex; align-items:center; gap:12px; flex-shrink:0; }
  .footer-socials a { width:36px; height:36px; border-radius:50%; display:flex; align-items:center; justify-content:center; background:var(--surface2); border:1px solid var(--border2); color:var(--muted); transition:color .15s, border-color .15s; }
  .footer-socials a svg { width:16px; height:16px; }
  .footer-socials a:hover { color:var(--text); border-color:var(--dim); }
  .footer-links { display:flex; flex-wrap:wrap; gap:20px; padding:20px 0; }
  .footer-links a { font-size:12.5px; color:var(--muted); text-decoration:none; }
  .footer-links a:hover { color:var(--text); }
  .footer-bottom { font-size:11.5px; color:var(--dim); }
  .footer-disclaimer { margin:10px 0 0; max-width:90ch; font-size:11px; line-height:1.6; color:var(--dim); }

  /* Filter chips (Luke, 2026-09-15: "introduce filters... transactions/
     Watch Tokens/Boosts/Tickets/Rewards") — same pill shape as
     .alert-filter-chip elsewhere, single-select (only .active carries
     color; no per-category palette since these aren't severity-coded
     like alerts). */
  .wallet-history-filters { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:14px; }
  .wallet-history-filter-chip { font-size:11.5px; padding:5px 12px; border-radius:var(--r-pill); border:1px solid var(--border2); background:var(--surface2); color:var(--muted); cursor:pointer; font-family:inherit; white-space:nowrap; }
  .wallet-history-filter-chip.active { background:var(--text); border-color:var(--muted); color:var(--bg); font-weight:600; }

  /* Month grouping (Luke, 2026-09-15: "grouped by months. all previous
     months are collapsed, except the current month") — a plain
     click-to-toggle header per month, same chevron-rotate affordance as
     .faq-q above. */
  .wallet-history-month { margin-bottom:10px; }
  .wallet-history-month-head { display:flex; align-items:center; gap:8px; padding:8px 2px; cursor:pointer; }
  .wallet-history-month-label { font-size:13px; font-weight:700; color:var(--text); }
  .wallet-history-month-count { font-size:11px; color:var(--muted); background:var(--surface2); border-radius:var(--r-pill); padding:1px 8px; }
  .wallet-history-month-head svg { margin-left:auto; color:var(--muted); transition:transform .2s; flex-shrink:0; }
  .wallet-history-month.open .wallet-history-month-head svg { transform:rotate(180deg); }
  .wallet-history-month .wallet-history-card { display:none; }
  .wallet-history-month.open .wallet-history-card { display:block; }

  .wallet-history-card { background:var(--surface); border:1px solid var(--border); border-radius:var(--r-card); overflow:hidden; }
  .wallet-history-item { display:flex; align-items:center; gap:10px; padding:11px 16px; border-bottom:1px solid var(--border); flex-wrap:nowrap; }
  .wallet-history-item:last-child { border-bottom:none; }
  /* Desktop/tablet: one line, ellipsis-truncated if the label is long —
     an unclipped label used to wrap across several lines and squeeze the
     fixed-width date/value columns until the row looked stacked. On
     mobile this flips (see the max-width:600px block below): Luke,
     2026-09-15: "the purchase history date should be on a different row
     to the purchase history item [description], which gives us more of
     a readable description" — there's no fixed-width neighbor stealing
     horizontal space from the label once the date has its own row, so
     it doesn't need truncating there either. */
  .wallet-history-date { font-size:11px; color:var(--muted); white-space:nowrap; flex-shrink:0; }
  .wallet-history-label { font-size:12.5px; color:var(--text); flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .wallet-history-values { display:flex; flex-direction:column; align-items:flex-end; gap:4px; flex-shrink:0; }
  .wallet-history-value { font-size:12.5px; font-weight:600; display:flex; align-items:center; gap:4px; white-space:nowrap; flex-shrink:0; }
  .wallet-history-value .ic { display:flex; }
  .wallet-history-value.green { color:var(--green); }
  .wallet-history-value.red { color:var(--red); }
  .wallet-history-value.muted { color:var(--muted); font-weight:500; }
  /* "Download invoice" — Stripe's own hosted invoice URL, Tom+Luke's
     "Invoices & receipts" ask (no PDF generator of our own needed). */
  .wallet-history-receipt-link { font-size:11px; font-weight:600; color:var(--brand); text-decoration:none; }
  .wallet-history-receipt-link:hover { text-decoration:underline; }
  @media (max-width:800px) {
    .wallet-history-item { flex-wrap:wrap; gap:2px 8px; padding:10px 12px; }
    .wallet-history-date { font-size:10px; width:100%; order:-1; }
    .wallet-history-label { flex:1 1 auto; white-space:normal; overflow:visible; text-overflow:clip; }
  }

  /* ── Add watch redesign ── */
  .add-watch-layout { display:block; }
  .mob-only { display:none; }
  /* Desktop: thin border around card+fields panel */
  .add-top-panel { border:1px solid var(--border2); border-radius:var(--radius-lg); padding:20px; margin-bottom:16px; }
  @media (max-width:860px) {
    .add-top-panel { border:none; padding:0; margin-bottom:0; }
    .mob-only { display:block !important; }
  }
  .add-section-label { font-size: 11px; color:var(--text); letter-spacing:.08em; text-transform:uppercase; margin-bottom:8px; margin-top:20px; }
  .add-section-label:first-child { margin-top:0; }
  .add-section-label:first-child.page_section_title { padding-top: 0 !important; }
  /* Manual mode: static "no market data" notice, no PokePulse lookup at all */
  .aw-nomkt { display:flex; gap:12px; align-items:flex-start; margin-top:16px; border:1px dashed var(--border2); border-radius:var(--radius-lg); padding:14px 16px; }
  .aw-nomkt .ic { width:32px; height:32px; border-radius:8px; background:var(--surface); display:flex; align-items:center; justify-content:center; flex-shrink:0; color:var(--muted); }
  .aw-nomkt .t { font-size:13px; font-weight:500; color:var(--text); }
  .aw-nomkt .h { font-size:12px; color:var(--muted); margin-top:3px; line-height:1.45; }
  /* One row: [Preview/Update listings] [Vinted logo] [eBay logo] (Luke,
     2026-10 button order). The action button takes the remaining space;
     the two logo chips are fixed-width so differently-proportioned brand
     marks (Vinted's wide wordmark vs eBay's squarer one) still line up. */
  .marketplace-row { display:flex; gap:8px; align-items:stretch; }
  .marketplace-chip { display:flex; align-items:center; justify-content:center; width:92px; flex-shrink:0; background:var(--surface); border:1px solid var(--border2); border-radius:var(--radius); padding:12px 18px; cursor:pointer; transition:border-color .15s, background .15s; }
  .marketplace-chip.active { border-color:var(--green); background:var(--green-bg); }
  .marketplace-chip.disabled { opacity:.4; cursor:not-allowed; }
  .marketplace-chip .mp-badge { font-size: 11px; background:var(--amber-bg); border:1px solid var(--amber-border); color:var(--amber); border-radius:4px; padding:1px 5px; }
  /* The logo itself IS the button's content now — no text label — so its
     active/inactive state has to read through the logo alone: full colour
     when watched, greyed out and dimmed when excluded. */
  .marketplace-chip .mp-logo { height:18px; width:auto; max-width:100%; display:block; filter:grayscale(1); opacity:.4; transition:filter .15s, opacity .15s; }
  .marketplace-chip.active .mp-logo { filter:none; opacity:1; }
  .search-market-btn { display:flex; align-items:center; justify-content:center; gap:8px; flex:1; padding:13px; background:var(--brand); color:var(--brand-contrast); border:none; border-radius:var(--radius); font-family:inherit; font-size:13px; font-weight:500; letter-spacing:.04em; cursor:pointer; transition:opacity .15s; }
  .search-market-btn:hover:not(:disabled) { opacity:.88; }
  .search-market-btn:disabled { opacity:.35; cursor:not-allowed; }
  .aw-duration-note { font-size:12px; color:var(--text-secondary); background:var(--surface2); border:1px solid var(--border); border-radius:var(--radius); padding:10px 12px; line-height:1.5; }
  .aw-duration-note b { color:var(--text); }
  /* Slot count / plan name — bold + brand color instead of a "Great
     news!" lead-in or a bold "indefinitely" (Luke, 2026-09-11: ditch
     both of those). */
  .aw-duration-note .aw-note-brand { color:var(--brand); }
  /* Boost cost note — "N boost(s)" in bold boost-blue, same accent used
     everywhere else a boost is spent or shown (--boost). */
  .aw-duration-note .aw-note-boost { color:var(--boost); }
  /* .aw-boost-cost-info now sits empty (innerHTML = '') always — Luke,
     2026-10-23: the boost-cost sentence folded into .aw-duration-info's
     own line instead (see renderAwSetupInfo's own comment: it was a
     second stacked box that pushed the hero's fixed-height, overflow:auto
     content past 350px). Originally just #aw-boost-cost-info (Manual's
     id-bearing copy): #aw-ready-card > .wd-card is a flex column with
     gap:10px between EVERY child, so an empty-but-still-display:block div
     still claimed a full gap on both sides of it (Luke, 2026-10-06: "on
     mobile the gaps and margins are still not right in the 'Defaults
     shown below...' card"). Broadened to the shared class so the merged
     hero's own class-only copy (no id) gets the same collapse — now that
     BOTH instances are unconditionally empty, this is what keeps either
     from claiming a stray 12px margin-top for nothing. */
  .aw-boost-cost-info:empty { display:none; }

  /* ── Add watch: mobile-first sequenced blocks ── */
  .aw-grid { display:flex; flex-direction:column; gap:16px; }
  /* Luke, 2026-10-10: "<800px mobile arrangement we need the cards in a
     stacked order of: Card Details, Boost, Refinement, Ready to go" —
     plain DOM order would stack Ready to go (it sits earlier in the
     markup than Refine settings, for Search mode's sake) ahead of
     Refinement instead. `order` works in this flex column exactly like it
     does in the ≥800px grid below, so the same three declarations just
     need to exist unscoped by that media query too — the ≥800px block
     further down overrides them again for its own desktop arrangement. */
  [data-aw-mode="manual"] .aw-area-freq { order:1; }
  [data-aw-mode="manual"] #aw-refine-panel { order:2; }
  [data-aw-mode="manual"] #aw-ready-card { order:3; }
  /* Luke, 2026-10-23: "Your selection" ("Ready to go" merged inside it)
     and "Boost alert speeds" sit beside each other at a genuine 2:1
     width, matching the redesign mockup. .aw-hero-row wraps
     #aw-market-result + .aw-area-freq so they
     can have their own column ratio, independent of .aw-grid's plain
     1fr/1fr — but Manual mode still needs .aw-area-freq to reorder
     against #aw-refine-panel/#aw-ready-card as a normal, direct .aw-grid
     item (see the order rules just above), so display:contents is the
     default here at every width: the wrapper itself never takes up a box,
     its children behave exactly as if they were direct .aw-grid children.
     Only Search mode, only ≥800px (below that .aw-grid is a single flex
     column anyway — see the ≥800px block further down), turns it into a
     real 2-column grid. */
  .aw-hero-row { display:contents; }
  .aw-block { background:var(--surface); border:1px solid var(--border2); border-radius:var(--radius-lg); padding:14px; }
  .aw-block-plain { padding:0; border:none; background:none; }
  /* Check live market is a .wd-card directly (no heading+card wrapper);
     Refine settings uses the wrapper+nested-card shape in Manual mode only
     (.aw-area-criteria > .wd-card, since 2026-10-10 — see #aw-refine-panel
     in dashboard.html) but stayed a direct .wd-card for Search mode's own
     single-box accordion. Either way .wd-card's base margin-bottom:10px
     just doubled up against .aw-grid's own consistent gap here, same
     redundant-spacing bug the ≥800px rule below already fixes for the
     other wrapper+nested-card blocks (Luke, 2026-10-06, from a devtools
     screenshot of the extra gap under Check live market's buttons). True
     at every width, not just mobile — .aw-grid's gap is active in both the
     mobile flex-column and the ≥800px grid layout. */
  .aw-area-mkts, .aw-area-criteria, .aw-area-criteria > .wd-card { margin-bottom: 0; }
  /* Luke, 2026-10-10 (annotated screenshot): the Include/Exclude keyword
     fields read as cramped/short — .form-grid's own max-width:560px (sized
     for the narrower two-column forms it's shared with elsewhere) was
     capping them well short of the actual card width once Refine settings
     became its own full-width row in Manual mode. Scoped to Manual only —
     Search mode's identical accordion (same .form-grid, same field ids)
     is explicitly "perfect" and stays exactly as it was. */
  [data-aw-mode="manual"] #aw-refine-panel .form-grid { max-width:none; }
  /* The search-mode accordion header (icon+title+chevron) that used to sit
     above this content is gone in Manual mode — #aw-refine-collapse is now
     the first (only) thing inside the card, so its own margin-top:16px
     (meant to clear that header) just became a stray gap at the top. */
  [data-aw-mode="manual"] #aw-refine-collapse { margin-top:0; }
  /* Luke, 2026-10-25: "here is my issue... do this" (struck the
     background:var(--surface) declaration) — this id-scoped rule
     (#pane-add .field input) outranked .aw-searchfield input's own
     background:none (class+type vs id+class+type), so the Manual mode
     fields' <input> itself got a solid var(--surface) fill sitting on top
     of/overlapping .aw-searchfield's own border and background, rather
     than staying transparent and letting the wrapper's own box be the
     only thing drawn — the real source of the "border inconsistency
     between tabs" and the mismatched-looking edges, not anything in
     .aw-searchfield itself. Gone entirely; the standard .field input/
     select's own --surface2 background (meant for --surface-toned panes
     like Settings) was never actually a problem here since nothing in
     Add Watch still uses a bare .field input besides Manual's threshold
     price fields, which already set their own background inline. */
  /* Luke, 2026-10-23: "why are the graded card and the boost card not on
     the same row ffs" — caught live at 888px, under the old 900px
     threshold these row+wrap rules used to be scoped to (since folded
     into the site's own 800px tablet/nav breakpoint below — see that
     block's comment). .aw-selrow (the art+info-card pairing) was never
     gated behind that breakpoint to begin with — this brings Find it
     graded/Boost alert speeds down to the same unscoped floor (501px —
     one above the true ≤500px mobile stack, where full-width Find it
     graded is the deliberately correct look, confirmed against earlier
     screenshots). Everything here duplicates (rather than lowers the
     threshold of) the equivalent ≥800px rules below, since those also
     carry the hero's 350px height-matching that's correctly 800px-only —
     the hero itself isn't 350px-tall below that width either. */
  @media (min-width: 501px) {
    /* Luke, 2026-10-25: "take graded card out from the containing card"
       — Manual mode's own Graded card (#aw-manual-slab-row) now pairs
       with #aw-boost-card inside .aw-area-freq exactly like Search
       mode's #aw-slab-row always has, so this row/wrap pairing (and the
       grid-column:1/-1 + flex:1/margin-bottom:0 rules below it,
       previously [data-aw-mode="search"]-gated from when Manual mode's
       .aw-area-freq only ever held the one card) is no longer
       mode-specific. */
    .aw-area-freq { display: flex; flex-direction: row; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
    .aw-area-freq > .wd-card { flex: 1; margin-bottom: 0; }
    /* Luke, 2026-10-24: "#aw-slab-row should only be locked to the 248px
       wide view when the viewport is between 801-1000 pixels wide.
       Otherwise, it should stick to the principle of min-width 320px with
       #aw-boost-card" — same flex:1/min-width:320px floor as #aw-boost-card
       itself here (501-800px); the 801-1000px-only fixed 248px lives in
       its own narrow override further down, right after the 801px block. */
    /* Luke, 2026-10-25: #aw-manual-slab-row (Manual mode's own Graded
       card) added alongside #aw-slab-row — same min-width:320px wrap
       floor either way. #aw-boost-card's own floor is no longer
       search-gated for the same reason (it now pairs with a card in both
       modes, not just Search's). */
    [data-aw-mode="search"] #aw-slab-row,
    [data-aw-mode="manual"] #aw-manual-slab-row { flex: 1; min-width: 320px; margin-bottom: 0; }
    #aw-boost-card { min-width: 320px; }
    /* Luke, 2026-10-24: "on above 500px we do not show the language
       text" — mobile-only now (the language dropdown next to the search
       field already covers it above 500px, the original reason it was
       removed entirely before being brought back for mobile parity with
       the Pokemon/Set/Artist lightbox, which has no such dropdown of its
       own to be redundant with). */
    #aw-market-result .aw-mkt-language { display: none; }
  }
  /* Plain 2-column grid (≥801px, matching the site's own tablet/nav
     breakpoint — max-width:800px/min-width:801px, e.g. the nav hamburger
     at :2688 — rather than its old separate 900px, and corrected
     2026-10-24 from an initial min-width:800px that was off by one
     against that convention — Luke: "are there multiple breakpoints
     around the 800 mark? some less than equal to and others just less
     than?") — cards lay out in DOM order two-per-row (Search+Selection,
     Find it faster+Ready to go, Refine+Market) instead of each pinning to
     a named cell. Replaced the earlier named-area grid (2026-09-11) once
     the page moved from "5 fixed steps" to a shorter always-visible set
     plus two on-demand reveal panels (Luke, 2026-10: "find the card, then
     set up the watch straight away, check live markets, or refine
     settings" — see CHANGELOG) — a fixed area per section no longer
     matched a flow where panels appear/disappear. */
  @media (min-width:801px) {
    .aw-grid { display:grid; grid-template-columns:1fr 1fr; gap:16px; align-items:stretch; }
    .aw-full { grid-column:1/-1; }
    /* min-width:0 on every grid item overrides CSS Grid's default
       min-width:auto, which otherwise refuses to shrink a column below its
       content's min-content width — normally invisible, but a long
       unbroken string (e.g. a card's promoInfo with no natural break
       point, nowrap+ellipsis'd via .wd-sub) forced a whole column wider
       than its actual 1fr track and off the edge of the page (Luke: a card
       search result for "Tamamushi University \"Hyper Test\" 1998"
       reproduced it). Applies to all six areas, not just the ones known to
       hold card text today — any of them could end up with unbroken
       content later. */
    .aw-grid > * { min-width:0; }
    /* Each row's pair matches height (Luke: "make this look more balanced
       on desktop") instead of top-aligning with mismatched bottoms — most
       visible once a card's picked and the Search card collapses to a
       thin chip next to the much taller Your selection card. The grid
       item itself stretches automatically; wrapper sections (a heading
       plus a nested .wd-card/.aw-block, rather than being the bordered
       box themselves) also need to be flex columns with that inner box
       set to flex:1, so the actual visible border grows to fill it. */
    .aw-area-cardinfo, .aw-area-marketres, .aw-area-freq, #aw-ready-card { display:flex; flex-direction:column; }
    /* Luke, 2026-10-23: "when this page narrows, 'find it graded' and
       'boost alert speeds' should be the first to wrap... this happens
       [to .aw-selrow instead]" — between this 900px breakpoint and the
       hero-row's own higher one below, #aw-market-result/.aw-area-freq
       would otherwise get paired up by THIS grid's plain 1fr/1fr columns
       (same as every other row on the page) — not wide enough for
       .aw-selrow's art+320px-min-width card, so the thumbnail+info card
       pairing broke apart and wrapped first instead, while the much-too-
       wide Find it graded/Boost column sat there with yawning empty
       space, exactly backwards from what should give way first. Full
       width here (grid-column:1/-1 — a static, unconditional rule, inert
       everywhere .aw-grid isn't a real grid) keeps both stacked as their
       own full-width rows until the hero-row's own breakpoint overrides
       this back to grid-column:auto and takes over the pairing itself. */
    [data-aw-mode="search"] #aw-market-result { grid-column: 1/-1; }
    /* Luke, 2026-10-25: no longer search-only — Manual mode's own Graded
       card (#aw-manual-slab-row) pairs with #aw-boost-card here now too,
       so .aw-area-freq needs the same full-width breakout regardless of
       mode (previously only Search mode had two cards to pair). */
    .aw-area-freq { grid-column: 1/-1; }
    /* .wd-card's own margin-bottom:10px (meant for stacking several cards
       in one column) left this one sitting 10px short of the stretched
       Search box next to it — flex:1 fills the row height either way, but
       that trailing margin ate into the visible border box instead of
       disappearing, reading as the Search card being too tall by
       comparison (Luke: "approx 5px too tall once the your selection card
       is visible"). There's only ever one card here, so drop it. Same fix
       for Find it faster / Ready to go — same wrapper+nested-card pattern,
       same mismatch (Luke's annotated screenshot of that row's heights). */
    .aw-area-cardinfo > .aw-block, .aw-area-marketres > .wd-card,
    .aw-area-freq > .wd-card, #aw-ready-card > .wd-card { flex:1; margin-bottom:0; }
    /* Luke, 2026-10-23: "I don't feel that #aw-slab-row and #aw-boost-card
       should split to separate rows at the 900px breakpoint — they should
       stay on the same row until #aw-boost-card has to wrap as it can't
       go smaller than 320px wide" — same pattern .aw-selrow already uses
       for the art+info-card pairing above (flex-wrap + a min-width floor
       on the flexible child), instead of a hard breakpoint toggling
       between row and column.
       Luke, 2026-10-25: widened from Search-mode-only — Manual mode's own
       Graded card (#aw-manual-slab-row) now pairs with #aw-boost-card the
       same way Search's #aw-slab-row always has (the comment this
       replaces noted Manual mode "holds a different single card" — no
       longer true).
       Luke, 2026-10-23: "why does this happen between 1000-900px... it's
       the fact that for some reason those elements have gone really
       tall" — align-items defaults to stretch, and the row's own
       min-height:350px (below, meant to match the hero column when
       .aw-area-freq sits BESIDE it at ≥1000px) was also forcing this
       side-by-side pairing to stretch to 350 regardless of actual
       content — Find it graded's own content (toggle + two rows of
       chips, ~150-190px) doesn't come close, so it stretched into a tall
       card that's mostly empty padding. flex-start here so each card
       sizes to its own natural height instead of being forced to match
       its neighbor (min-height:350px itself stays Search/hero-only —
       Manual mode has no hero to match). */
    .aw-area-freq { flex-direction: row; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
    /* Luke, 2026-10-23 (fine-tune): "#aw-slab-row should be 248px wide,
       matching the width of the pokemon card" — fixed width regardless of
       whether it's paired beside #aw-boost-card or (once that's wrapped
       below) alone on its own line.
       Luke, 2026-10-24: narrowed to 801-1000px only — "otherwise it should
       stick to the principle of min-width 320px with #aw-boost-card" — so
       this unconditional rule drops back to that same flex:1/min-width:320px
       floor (set in the 501px block above, inherited here unless overridden
       below), and the bounded override right after this block's closing
       brace reinstates the fixed 248px for exactly 801-1000px, the width
       where it was originally tuned to match the hero's thumbnail column. */
    #aw-slab-row { margin-bottom: 0; }
    /* "...and the boost card should match the width of .aw-selrow
       .wd-card" — flex:1 (from the .aw-area-freq > .wd-card rule above)
       already does that for free once paired beside #aw-slab-row's own
       fixed 248px, same mechanism as .wd-card beside the art thumbnail;
       min-width:320px is the actual wrap trigger Luke described — below
       that it drops to its own full-width line instead of being crushed
       thinner, same pattern as .aw-selrow .wd-card's own floor. */
    /* No longer search-gated (2026-10-25) — #aw-boost-card pairs with a
       card in both modes now. */
    #aw-boost-card { min-width: 320px; }
    /* Manual mode's needs and order are different (Luke, 2026-10: "the
       order and needs are different - find it faster becomes #2, #3 is
       Refinement (which is a must do for manual) and #4 is the ready to
       go") — no Search/Selection pairing or Check live market exists in
       manual mode, so `order` reflows Refine settings (last in DOM, after
       Check live market) ahead of Find it faster without moving any markup
       that Search mode still relies on being where it is.
       Luke, 2026-10-25: "what is with the massive layout gaps on desktop
       on Manual?" — the pairing this comment used to describe ("Enter
       your card + Refine settings on row 1") no longer exists: #aw-card-
       details (the "Enter your card" fields box) was deleted outright
       once its own content moved to the bare #aw-manual-controls/.aw-nomkt
       blocks earlier this session, and .aw-area-mkts (Check live market)
       is Search-only, so #aw-refine-panel/#aw-ready-card were each left
       alone in their own grid row with no sibling to fill the second
       column — CSS Grid doesn't stretch a lone auto-placed item across
       empty tracks next to it, so that second column just sat empty.
       grid-column:1/-1 (Search mode's own .aw-area-freq already uses this
       same fix for the identical reason) makes each take the full row
       instead. */
    [data-aw-mode="manual"] #aw-refine-panel,
    [data-aw-mode="manual"] #aw-ready-card { grid-column: 1/-1; }
    /* Luke, 2026-10-25: "Find it graded and Boost alert speeds should
       always be ABOVE the Refine settings options" — this desktop block
       had them reversed from the mobile-first base order a few lines up
       (.aw-area-freq:1/#aw-refine-panel:2 there, flipped here to
       #aw-refine-panel:1/.aw-area-freq:2) — now consistent at every
       width. */
    [data-aw-mode="manual"] .aw-area-freq { order:1; }
    [data-aw-mode="manual"] #aw-refine-panel { order:2; }
    [data-aw-mode="manual"] #aw-ready-card { order:3; }
    /* Check live market / Refine settings are accordions, each opening
       independently — they shouldn't stretch to match each other's height
       the way the rows above do (Luke: "as these are accordion cards...
       do not need to balance in height"). Opt out of the grid's own
       align-items:stretch so each sizes to its own open/closed content.
       Manual mode's Ready to go (just the Start watching button) gets the
       same opt-out so it doesn't stretch tall to match Find it faster
       sitting next to it in row 2 — Search mode's own Ready to go still
       pairs with (and matches height to) Find it faster, so this is
       manual-mode-only. */
    .aw-area-mkts, #aw-refine-panel { align-self:start; }
    [data-aw-mode="manual"] #aw-ready-card { align-self:start; }
  }
  /* Luke, 2026-10-24: "#aw-slab-row should only be locked to the 248px
     wide view when the viewport is between 801-1000 pixels wide" — the
     width it was originally tuned to match the hero thumbnail column
     (Luke, 2026-10-23: "#aw-slab-row should be 248px wide, matching the
     width of the pokemon card"), which only holds true in this specific
     band: below 801px the hero isn't paired with Find it graded/Boost at
     all (full-width stacked rows — see the 801px block's own comment),
     and at ≥1000px the hero-row's 2:1 grid changes the available width
     again. max-width:999px (not 1000px) to butt cleanly against the
     ≥1000px block right below with no one-pixel overlap, same
     max-width:800px/min-width:801px pairing convention this file uses
     throughout — caught live, "just check my breakpoints are correct
     there." Outside this band #aw-slab-row falls back to the same
     flex:1/min-width:320px floor #aw-boost-card always uses (set in the
     501px block above). */
  @media (min-width: 801px) and (max-width: 999px) {
    [data-aw-mode="search"] #aw-slab-row { flex: 0 0 248px; min-width: 0; }
  }
  /* Luke, 2026-10-24: "when the .wd-nudge is visible I also want the height
     of #aw-slab-row to match #aw-boost-card" — native flex stretch instead
     of a guessed fixed height, since #aw-boost-card's own height varies
     with its content (boost ladder copy can wrap). 688px (not 801px) to
     match .aw-slab-nudge's own visible range exactly (see that rule's
     comment near .wd-nudge). Placed after the block above specifically —
     same [data-aw-mode="search"] .aw-area-freq selector, same specificity
     as the align-items:flex-start rule in the 801px block earlier in the
     file, so source order is what actually wins this tie (this file's
     recurring cascade gotcha); outside this band (and anywhere .wd-nudge
     is hidden) that earlier flex-start rule applies as normal, each card
     sized to its own natural height. */
  @media (min-width: 688px) and (max-width: 999px) {
    [data-aw-mode="search"] .aw-area-freq { align-items: stretch; }
  }
  /* Luke, 2026-10-23: the hero-row's own 2:1 split needs real room —
     art(250px, from aspect-ratio:5/7 off its 350px height) + gap(16px) +
     the info card's own 320px min-width = 586px minimum for its 2fr
     track alone. That doesn't reliably fit within the 800px block above
     (2fr of a ~800px-32px-padding grid is only ~512px), so the hero-row
     grid activates at a separate, higher breakpoint instead — below it,
     the 800px block's own unconditional grid-column:1/-1 (see its own
     comment) keeps #aw-market-result/.aw-area-freq stacked as full-width
     rows rather than a cramped, broken-looking pairing. 1000px clears
     586px needed with margin for scrollbar/zoom variance; this is a
     genuine third breakpoint (Luke, 2026-10-24, accepted after confirming
     the hero split can't be squeezed into the 800px tier without looking
     cramped) rather than a tier the rest of the page shares — nothing
     else on the page changes shape at 1000px. */
  @media (min-width: 1000px) {
    [data-aw-mode="search"] .aw-hero-row { display:grid; grid-template-columns:2fr 1fr; gap:16px; grid-column:1/-1; align-items:stretch; }
    [data-aw-mode="search"] #aw-market-result,
    [data-aw-mode="search"] .aw-area-freq { grid-column: auto; }
    /* Luke, 2026-10-23 (fine-tune): "the combined height of the open Find
       it graded and the Boost Alerts speeds should also be 350px" to
       match the hero column alongside it. Moved here from the 800px
       block above — min-height:350px only makes sense once .aw-area-freq
       is actually BESIDE the hero (this breakpoint); in the 800-999px
       band .aw-area-freq's two children sit beside EACH OTHER instead,
       and forcing that pairing to 350px stretched whichever card had
       less natural content (usually Find it graded) into a mostly-empty
       box — see this rule's old location for that bug's own writeup. */
    [data-aw-mode="search"] .aw-area-freq { min-height: 350px; }
  }
  .aw-reveal[hidden] { display:none !important; }
  .aw-reveal { animation:awFadeIn .35s ease both; }
  @keyframes awFadeIn { from{opacity:0;transform:translateY(8px);} to{opacity:1;transform:none;} }
  @media (prefers-reduced-motion:reduce) { .aw-reveal { animation:none; } }
  /* #aw-ready-card specifically: no reveal animation at all (Luke,
     2026-10-05: "make sure that the start watching button is always on
     top... sometimes falls behind the refine settings"). Any element with
     an active CSS animation targeting `transform` OR `opacity` — even one
     that settles on transform:none / opacity:1 — both (a) becomes the
     containing block for a position:fixed descendant, and (b) is promoted
     to its own stacking context for as long as animation-fill-mode:both
     holds it "actively animating". An opacity-only fade (tried first) still
     hit (b): #aw-ready-card became a stacking context with no explicit
     z-index, so .aw-sticky-cta's z-index:40 only won *inside* that
     context — Refine settings, with no animation and sat later in the DOM,
     painted its own (non-isolated) content on top of the whole thing
     regardless. Dropping the animation removes both problems at once; the
     card still appears the instant awRevealReady() clears its [hidden].
     Luke, 2026-10-23: "this one goes crazy with the start watch button" —
     #aw-market-result reintroduced exactly this bug once its own
     .aw-sticky-cta started docking to the bottom on mobile too (see that
     rule's own comment) — it also carries .aw-reveal but never got this
     same fix, so on mobile its still-animating box became the containing
     block for its OWN fixed-position button, pinning it to #aw-market-
     result's own edges instead of the viewport's. Same fix, same reason. */
  #aw-ready-card.aw-reveal,
  #aw-market-result.aw-reveal { animation: none; }


  /* Check live market / Refine settings — each a single half-width
     wd-card whose header row (modelled on .aw-manual-link's icon+text+chev
     but larger) toggles its own content open/closed in place, accordion
     style (awToggleMarket()/awToggleRefine() in dashboard.js). */
  .aw-choice-row { display:flex; align-items:center; gap:14px; cursor:pointer; }
  .aw-choice-row .ic { width:40px; height:40px; border-radius:10px; background:var(--surface2); display:flex; align-items:center; justify-content:center; flex-shrink:0; color:var(--text); }
  .aw-choice-row .t { flex:1; min-width:0; }
  .aw-choice-row .t .title { color:var(--text); font-weight:600; font-size:14.5px; }
  .aw-choice-row .t .sub { margin-top:2px; }
  .aw-choice-row .chev { color:var(--muted); flex-shrink:0; transition:transform .15s; }
  .aw-choice-row .chev.aw-chev-open { transform:rotate(180deg); }

  /* "Find my Watch" browse tabs (Luke, 2026-10-19) — Pokemon/Set/Artist/Search
     entry points above the existing Search/Manual flow. Underlined-tab style
     (not a segmented pill like .aw-mode-toggle below) — deliberately distinct
     so the two controls don't read as the same kind of choice. */
  .aw-browse-sub { font-size:14.5px; line-height:1.55; color:var(--muted); max-width:640px; margin:-6px 0 16px; }
  .aw-browse-tabs { display:flex; gap:4px; border-bottom:1px solid var(--border); margin-bottom:16px; }
  .aw-browse-tab { padding:12px 18px; background:none; border:none; border-bottom:2px solid transparent; color:var(--muted); font-size:14px; font-weight:500; font-family:inherit; cursor:pointer; }
  .aw-browse-tab.active { border-bottom-color:var(--brand); color:var(--text); font-weight:700; }
  .aw-browse-pane-off { display:none; }
  .aw-browse-soon { font-size:13px; color:var(--muted); padding:40px 0; text-align:center; }
  .aw-browse-grid-label { font-size:13px; font-weight:700; margin:20px 0 12px; }
  /* Popular Pokemon/Set/Artist tiles — each its own mini-card (border/
     shadow), NOT nested inside the surrounding .aw-block the search field
     sits in. Luke, 2026-10-20: "'Popular' item cards should have a
     min-width causing friendly wrapping on variable width devices...
     perhaps down to three columns wide on mobile... and a max of 5 wide on
     desktop" — auto-fit/minmax rather than a fixed 5-column grid (which
     had no mobile handling at all: 5 columns squeezed into a 375px screen
     would render ~60px slivers). Follow-up: "only handle 7 wide on
     desktop, can the maximum be 5 wide? and scale to 2 wide on 360px
     min-page width?" — fixed with a flat 140px floor + a 764px max-width
     cap (5 tiles' worth of space at that floor; 6 can never fit inside it).
     Second follow-up: "can we make the popular tiles expand in width so
     that they distribute and fill the full desktop width? but stick to
     the principles today with regards to the minimum width?" — the flat
     764px cap was the problem: on a wider pane it left dead space to the
     right instead of letting 5 columns stretch to fill it. Replaced with
     `max(140px, calc((100% - 64px) / 5))` as the minmax floor itself: at
     any container width, this evaluates to EXACTLY one-fifth of the
     available space (64px = 4 gaps between 5 columns) once that fifth
     exceeds 140px, so auto-fit always lands on exactly 5 full-width
     columns with no leftover gap — and still clamps to the original 140px
     mobile floor once a fifth would drop below it, preserving the same
     "down to 2 at 360px" behaviour as before. No max-width needed anymore:
     the calc term IS the cap. */
  .aw-pokemon-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(max(140px, calc((100% - 64px) / 5)), 1fr)); gap:16px; }
  .aw-pokemon-tile { display:flex; flex-direction:column; align-items:center; gap:6px; background:var(--surface); border:1px solid var(--border2); border-radius:var(--radius-lg); box-shadow:var(--shadow-sm); padding:14px 8px 12px; cursor:pointer; font-family:inherit; transition:border-color .15s, transform .15s, box-shadow .15s; }
  /* Glow colour is per-tile (--ring/--glow, set inline from the Pokemon's
     own type) rather than the generic shadow every other tile hover uses. */
  .aw-pokemon-tile:hover { border-color:var(--ring, var(--muted)); transform:translateY(-2px); box-shadow:0 0 22px var(--glow, transparent), var(--shadow-md); }
  .aw-pokemon-tile img { width:85px; height:72px; object-fit:contain; }
  /* Luke, 2026-10-20: "I also have provided an image for each artist/
     illustrator tile. ensure that they all have consistent displayed
     size." — the 20 supplied photos are each a different original size
     (roughly 650-785px wide, ~1.5-1.7:1 landscape, no two identical), so
     the shared 72×72 contain-fit box above (right for square Pokedex art/
     set logos) would have left each one a different visible size inside
     its own letterboxing. A fixed-aspect-ratio box with object-fit:cover
     instead fills the same footprint on every tile regardless of the
     source photo's own dimensions. */
  .aw-pokemon-tile img.artist-photo { width:100%; height:auto; aspect-ratio:3/2; object-fit:cover; display:block; }
  .aw-pokemon-tile .n { font-size:12.5px; font-weight:600; color:var(--text); text-align:center; }
  .aw-pokemon-tile .t { font-size:10px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; }
  /* Luke, 2026-10-20: "remove the padding around the artwork image in its
     tile, (almost in the same way we have the pokemon card results with
     their card container)" — same pattern as .aw-cardresult-tile/-thumb:
     the OUTER tile goes padding:0 + overflow:hidden, so its own rounded
     corners (border-radius above, inherited) clip the flush photo rather
     than the photo rounding itself (hence artist-photo's own radius was
     dropped above) — only the name label below keeps its own padding, the
     same way .aw-cardresult-tile .n/.meta do. :has() scopes this to just
     the artist tile without touching the Pokemon/Set tiles' own (still
     padded, still 72×72-contained) image + label layout. */
  .aw-pokemon-tile:has(.artist-photo) { padding:0; overflow:hidden; }
  .aw-pokemon-tile:has(.artist-photo) .n { padding:0 8px 10px; }

  /* Active search — swaps the tile grid for small text lozenges (Luke,
     2026-10-19: "we see lozenges... click Mew and Mewtwo") rather than
     filtering the same 20 image tiles, since the search covers the full
     species list, not just the 20 popular ones with art. Clicking one
     swaps the grid again, to that Pokemon's real cards (.is-cards below). */
  .aw-pokemon-grid.is-lozenges { display:flex; flex-wrap:wrap; gap:10px; }
  .aw-pokemon-lozenge { display:flex; align-items:center; gap:8px; padding:9px 16px; border-radius:var(--r-pill,20px); background:var(--surface); border:1px solid var(--border2); cursor:pointer; font-family:inherit; transition:border-color .15s, background .15s; }
  .aw-pokemon-lozenge:hover { border-color:var(--muted); background:var(--surface2); }
  .aw-pokemon-lozenge .n { font-size:13px; font-weight:600; color:var(--text); }
  .aw-pokemon-lozenge .t { font-size:10px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; }

  /* A lozenge picked — real cards from pokepulse_card_catalog (the same
     local search the Search tab's own picker uses), shown as a thumbnail
     grid rather than the Search tab's row list: 6 wide on desktop, stepping
     down through 5 and 4 to 3 on mobile (Luke, 2026-10-20). Tiles never
     grow past their 6-wide desktop size — each tier caps the GRID's own
     max-width to exactly N tiles at the 200px reference size + gaps, so a
     narrower column count never means a BIGGER tile, only a narrower grid
     (which centres rather than stretching to fill). Keep this in sync with
     AW_POKEMON_COLS_PAGE_SIZE / awPokemonCurrentCols() in dashboard.js —
     same breakpoints drive the pagination page size (6=12, 5=10, 4=12,
     3=9 — a whole number of rows each time). */
  .aw-pokemon-grid.is-cards { display:grid; grid-template-columns:repeat(6, minmax(0, 1fr)); gap:16px; max-width:1280px; }
  @media (max-width:1199px) {
    .aw-pokemon-grid.is-cards { grid-template-columns:repeat(5, minmax(0, 1fr)); max-width:1064px; }
  }
  @media (max-width:959px) {
    .aw-pokemon-grid.is-cards { grid-template-columns:repeat(4, minmax(0, 1fr)); max-width:848px; }
  }
  @media (max-width:719px) {
    .aw-pokemon-grid.is-cards { grid-template-columns:repeat(3, minmax(0, 1fr)); gap:10px; max-width:620px; }
  }
  /* No outer card/border here (Luke, 2026-10-19: "cards do not need to be
     in cards") — the thumbnail already reads as a card, boxing it again
     was redundant. Just the image + text, background/border/shadow live
     only on the thumbnail hover for feedback. No fixed aspect-ratio box
     either (Luke: "remove the space between the... image and the card
     name" — a boxed ratio that didn't exactly match the real card image's
     proportions was letterboxing, showing as a gap) — the image sets its
     own height, so there's no reserved space bigger than the art itself. */
  /* Back in a container card (Luke, 2026-10-19: "put the pokemon card,
     within a card... that encapsulates it and the card name, number and
     set... just no padding on the container card") — image flush to the
     container's edges, text sits directly under it with no inner inset. */
  /* No hover state on the container itself (Luke, 2026-10-20: "the watch
     eye is enough") — only the thumbnail's eye overlay responds to hover. */
  /* Top corners scale with the tile's own width (Luke, 2026-10-20: "4% of
     its width") rather than a flat px value — bottom corners stay flat 5px
     (not mentioned, left as-is). */
  /* Horizontal/vertical radius split (Luke, 2026-10-20) — a plain "%" would
     resolve the vertical component against the tile's HEIGHT, and these
     tiles are taller than wide (5:7 card ratio), so a single percentage
     reads as a stretched ellipse rather than a round corner. 4.5% width /
     3% height keeps the actual pixel radius close to equal in both
     directions (0.045w vs 0.042w at this ratio). */
  .aw-cardresult-tile { display:flex; flex-direction:column; gap:6px; background:var(--surface); border:1px solid var(--border2); border-radius:8px; border-top-left-radius:5.5% 3.5%; border-top-right-radius:5.5% 3.5%; overflow:hidden; padding:0; cursor:pointer; font-family:inherit; text-align:left; }
  /* Same hover-eye "watch this card" affordance as the dashboard's Weekly
     Best Sellers (.mkt-art-eye) and home.html's seller-thumb-wrap — one
     shared visual language for "this card tile is actionable." Bottom
     corners get their own 5px radius (top corners are already clipped by
     the container's own overflow:hidden above) — Luke, 2026-10-20. */
  .aw-cardresult-thumb { width:100%; display:flex; position:relative; overflow:hidden; border-radius:0 0 5.5% 5.5% / 0 0 3.5% 3.5%; }
  .aw-cardresult-thumb img { width:100%; height:auto; display:block; }
  /* No thumbnail at all, or a real one that 404/403'd (Luke, 2026-10-20 —
     some PokePulse "Play! Pokemon Prize Pack Series Two" thumbnails return
     403 even though the catalog has a URL on file) — card-shaped icon
     placeholder instead of a collapsed sliver or a blank broken image. */
  .aw-cardresult-thumb.is-fallback { aspect-ratio:5/7; align-items:center; justify-content:center; background:var(--surface2); color:var(--muted); }
  .aw-cardresult-thumb.is-fallback svg { width:32px; height:32px; }
  .aw-cardresult-eye {
    position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    background:rgba(0,0,0,.6); color:#fff; opacity:0; transition:opacity .15s;
  }
  .aw-cardresult-tile:hover .aw-cardresult-eye { opacity:1; }
  .aw-cardresult-eye svg { width:28px; height:28px; }
  /* material + promo_info pills (Luke, 2026-10-20: "sometimes we see some
     of the same card but we might see a slight difference... a tiny pill
     that possibly shows the material, and the promo_info" — then: "don't
     put them in the same pill... two pills one on top of each other,
     material top, promo bottom. right aligned, always truncated") —
     distinguishes two otherwise-identical-looking tiles (same name/number/
     set), e.g. a plain "Holo" pill stacked over its own separate "1st
     Edition" pill. pointer-events:none so this never blocks the hover-eye
     overlay sitting on top of it in the same corner. */
  .aw-cardresult-variants {
    position:absolute; right:4px; bottom:4px; max-width:calc(100% - 8px);
    display:flex; flex-direction:column; align-items:flex-end; gap:2px;
    pointer-events:none;
  }
  .aw-cardresult-variant {
    max-width:100%; background:rgba(0,0,0,.65); color:#fff; font-size:9px;
    font-weight:700; letter-spacing:.01em; padding:2px 6px; border-radius:999px;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .aw-cardresult-tile .n { padding-left:6px; font-size:12px; font-weight:600; color:var(--text); line-height:1.3; }
  /* No price on this screen (Luke, 2026-10-20: "we never show prices on
     this screen. ONLY in the traditional card search") — number/set shifts
     up 2px and gets its own bottom padding since it's now the last line. */
  .aw-cardresult-tile .meta { display:block; margin-top:-2px; padding:0 6px 5px; font-size:10.5px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  /* Search tab only (#aw-pick is unique to it), AND only a tile that
     actually has a price line below .meta — a tile with no price on file
     keeps the base rule's original 5px, same as the Pokemon/Set/Artist
     tabs where .meta is the tile's last line (Luke, 2026-10-23: "if there
     is no price on the page at all... keep the original 5px"). */
  #aw-pick .aw-cardresult-tile.has-price .meta { padding:0 6px 1px; }
  /* Search tab only (Luke, 2026-10-20: "ONLY in the traditional card
     search") — same tile component as the browse tabs, plus a price line
     the browse tabs' own tiles deliberately don't have (see awRenderResults). */
  .aw-cardresult-tile .price { display:flex; align-items:center; gap:5px; padding:0 6px 7px; margin-top:-3px; font-size:11.5px; font-weight:700; color:var(--text); }
  .aw-cardresult-tile .price .trend { font-size:10px; font-weight:600; color:var(--green); display:flex; align-items:center; gap:1px; }
  .aw-cardresult-tile .price .trend.down { color:var(--red); }
  /* Search tab results stay inline-selectable (no lightbox) — a highlighted
     border + corner check take the place of .aw-prow.sel's old green-tinted
     row background. */
  .aw-cardresult-tile.sel { border-color:var(--brand); box-shadow:0 0 0 1px var(--brand) inset; }
  .aw-cardresult-check { position:absolute; top:6px; right:6px; background:var(--green); color:#fff; border-radius:50%; width:22px; height:22px; display:flex; align-items:center; justify-content:center; box-shadow:0 1px 4px rgba(0,0,0,.35); }

  /* Skeleton layout (Luke, 2026-10-20: "some form of skeleton layout from
     the moment we try to load new cards") — reuses the shared .sk/.sk-line
     shimmer system (same one Shop/Referral already use) rather than a
     one-off loading spinner, so a fresh Pokemon search or a filter/sort
     change fills the grid with card-shaped placeholders immediately. */
  .aw-cardresult-skeleton { cursor:default; }
  .aw-cardresult-skeleton .aw-cardresult-thumb.sk { aspect-ratio:5/7; border-radius:0; }
  .aw-cardresult-skeleton .sk-line { margin:0 6px; }
  .aw-cardresult-skeleton .sk-line:last-child { margin-bottom:5px; }

  /* Result count, right-aligned next to "Filters:"/"Popular Pokémon"
     (desktop — wraps naturally under narrow widths rather than a media
     query, since it's just inline text). */
  .aw-browse-grid-label-row { display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin:20px 0 12px; }
  .aw-browse-grid-label-row .aw-browse-grid-label { margin:0; }
  .aw-pokemon-result-count { font-size:12px; color:var(--muted); white-space:nowrap; }

  /* Card lightbox (Luke, 2026-10-22: "now that we see card thumbnails, I
     want to be able to open a lightbox that is the equivalent of" the
     Search tab's own ready-to-watch panel, "but laid out in a cleaner
     format") — reuses the shared .overlay/.modal shell every other modal
     in the app already uses, just its own inner layout. "close button
     doesn't resemble other close buttons in the top right" — turned out
     the X itself was never styled at all (.x-close isn't a real class
     anywhere in this file, just a name invented when writing this one) —
     every other modal's X is the same inline style instead of a shared
     class (boost modal, basket, confirm, beta welcome, bug report), copied
     verbatim onto this one too rather than finally giving it one. */
  /* Luke, 2026-10-22: "the card thumbnail needs to be at least the same
     size as it was on the desktop page prior to the lightbox opening...
     double it's current size" — 90px to 180px; the modal's own max-width
     widens with it (dropping the 380px cap for the default 480px every
     other modal in the app uses) so the text column next to a much bigger
     thumbnail still has room to breathe. */
  /* Luke, 2026-10-22: "The modal should have a min-width of 340px" — the
     base .modal class only has width:480px/max-width:95vw, no floor, so a
     narrow enough viewport (95vw < 340px, i.e. under ~358px wide) would
     otherwise keep shrinking it past that. */
  .lb-modal { text-align:left; position:relative; min-width:340px; }
  /* Luke, 2026-10-22: "undo that change. all data and the watch button to
     the right of the pokemon card, except the Plan specific section and
     the detail[s] and modify settings section which is all full width" —
     a single right-hand column (name, meta, State/Language/Artist, price,
     Start watching) beside the thumbnail; only the quota box and the
     defaults/modify-settings footer below span full width. Follow-up:
     "align the start button to the bottom of the pokemon card" — the
     button sits in its own grid row/area (not just flex-pushed within the
     text column) specifically so the SAME button, unmoved in the DOM, can
     relocate to its own full-width row on mobile below — see the
     max-width:500px block. CSS Grid named areas (not flex) is what makes
     that possible without touching dashboard.js per breakpoint. */
  .lb-head {
    display:grid;
    grid-template-columns:180px 1fr;
    grid-template-rows:auto 1fr;
    grid-template-areas:"thumb text" "thumb button";
    gap:12px 16px;
    margin-bottom:16px;
  }
  .lb-thumb { grid-area:thumb; width:100%; aspect-ratio:5/7; border-radius:5px; border-top-left-radius:4.5% 3%; border-top-right-radius:4.5% 3%; overflow:hidden; background:var(--surface2); }
  .lb-thumb img { width:100%; height:100%; object-fit:cover; display:block; }
  .lb-thumb.is-fallback { display:flex; align-items:center; justify-content:center; color:var(--muted); }
  .lb-head-text { grid-area:text; min-width:0; padding-top:4px; display:flex; flex-direction:column; gap:8px; }
  /* Luke, 2026-10-22: "text should not wrap in this modal for the card
     stats... it should truncate" — .lb-row .lb-v already did (below); name
     and meta are now the same (a long card name/set name squeezed against
     an 1/3-width mobile thumbnail would otherwise wrap and blow out the
     row heights instead of just clipping with an ellipsis). */
  .lb-name { font-size:16px; font-weight:800; color:var(--text); line-height:1.25; margin-bottom:-4px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .lb-meta { font-size:12.5px; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .lb-rows { display:flex; flex-direction:column; gap:3px; }
  .lb-row { display:flex; gap:6px; font-size:12px; line-height:1.5; }
  /* "Line the State, Language and Artist values to the same starting
     line" — min-width wasn't wide enough to fit "Language" (the longest
     label), so its value started further right than State/Artist's. Wide
     enough for "Language" at this font-size, with room to spare. */
  .lb-row .lb-k { color:var(--muted); min-width:64px; flex:none; }
  .lb-row .lb-v { color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .lb-price-row { display:flex; align-items:baseline; gap:8px; background:var(--surface2); border:1px solid var(--border2); border-radius:var(--radius); padding:8px 12px; min-height:38px; }
  .lb-price-main { display:flex; align-items:baseline; gap:8px; }
  /* Luke, 2026-10-23: "pulse logo needs to be 20% bigger on this modal" —
     marketPriceIconSvg() (dashboard.js) always renders at a fixed 14x14,
     reused everywhere a price shows; scoped here rather than touching that
     shared function, since this is specifically about the lightbox's own
     price box. 14 * 1.2 = 16.8, rounded to 17. The base .market-price-icon
     rule (elsewhere in this file) also sets flex:0 0 14px — inside a flex
     row (.lb-price-main), flex-basis governs the main-axis size ahead of
     width, so width:17px alone silently did nothing to the actual
     rendered width (only height moved) until flex-basis was overridden
     too — same reason every OTHER per-context override of this icon
     (.mkt-card-sub, .wd-mkt-price) sets flex alongside width/height, not
     just width/height on their own. */
  .lb-price-main .market-price-icon { width:17px; height:17px; flex:0 0 17px; }
  /* "Increase the market price" */
  .lb-price-val { font-size:22px; font-weight:800; color:var(--text); }
  .lb-price-lbl { font-size:11px; color:var(--muted); }
  /* "take the updated text outside of the tiles" — used to be nested
     inside .lb-price-row itself; now its own line below the box, same
     spot the real panel's own "Updated ..." sub-line sits in. */
  .lb-price-updated { font-size:10.5px; color:var(--muted); margin-top:-2px; }
  .lb-start-btn { grid-area:button; align-self:end; }
  /* Luke, 2026-10-22: "At 500px make the pokemon card 130px width and move
     the start watch button below. remove those labels... and that should
     be the extent of the change" — one single breakpoint, three fixed
     changes: thumbnail 180px → 130px, button relocates below (full width,
     own row), State/Language/Artist labels hidden (values stay). No
     scaling in between — simple on/off switch at 500px, nothing more. */
  @media (max-width:500px) {
    .lb-head { grid-template-columns:130px 1fr; grid-template-areas:"thumb text" "button button"; }
    .lb-start-btn { margin-top:4px; }
    .lb-row .lb-k { display:none; }
    /* "remove the word 'market'" (below 500px) — the JS always renders it
       now (see awLightboxLoadPrice), hidden here instead so re-introducing
       it above 500px needed zero JS changes, just dropping this rule.
       Scoped to .lb-price-main specifically — .lb-price-lbl is ALSO the
       class on the "No market price on file yet" fallback message (a
       bare sibling, not inside .lb-price-main), which must stay visible
       at every width regardless of whether a real price was found. */
    .lb-price-main .lb-price-lbl { display:none; }
  }
  /* "Centralise the Elite Tier text" — .aw-duration-note's own real CSS
     (elsewhere in this file) doesn't set text-align, so it inherits
     whatever its container uses; scoped to just this box rather than
     changing that shared class everywhere else it's used. */
  .lb-quota .aw-duration-note { text-align:center; }
  /* "don't explain that the percentages are the defaults... Make it like
     this" (the real panel's own plain colored line, no lead-in sentence) */
  .lb-defaults-note { font-size:11.5px; color:var(--muted); text-align:center; margin-top:10px; }
  .lb-modify-link { display:block; width:100%; background:none; border:none; padding:8px; margin-top:6px; font-family:inherit; font-size:12.5px; font-weight:600; color:var(--muted); text-align:center; cursor:pointer; text-decoration:underline; text-underline-offset:2px; }
  .lb-modify-link:hover { color:var(--text); }

  /* Card-results-only filters — rarity/finish chips (multi-select, like
     .marketplace-chip) + a New/Old sort toggle (segmented, like
     .aw-mode-toggle). */
  .aw-pokemon-filters { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:12px; margin-bottom:14px; }
  .aw-filter-chips { display:flex; flex-wrap:wrap; gap:8px; }
  .aw-filter-chip { padding:7px 14px; border-radius:var(--r-pill,20px); background:var(--surface); border:1px solid var(--border2); color:var(--muted); font-size:12.5px; font-weight:600; cursor:pointer; font-family:inherit; transition:border-color .15s, background .15s, color .15s; }
  .aw-filter-chip:hover { border-color:var(--muted); }
  .aw-filter-chip.active { border-color:var(--brand); background:var(--brand-bg); color:var(--brand); }
  .aw-sort-row { display:flex; align-items:center; gap:8px; }
  .aw-sort-label { font-size:12px; color:var(--muted); white-space:nowrap; }
  .aw-sort-toggle { display:flex; background:var(--surface); border:1px solid var(--border2); border-radius:var(--radius); padding:3px; }
  .aw-sort-btn { padding:7px 12px; border-radius:calc(var(--radius) - 2px); font-size:12px; color:var(--muted); cursor:pointer; background:none; border:none; font-family:inherit; white-space:nowrap; }
  .aw-sort-btn.active { background:var(--surface2); color:var(--text); font-weight:600; }

  .aw-mode-toggle { display:flex; background:var(--surface); border:1px solid var(--border2); border-radius:var(--radius); padding:3px; margin-bottom:14px; }
  .aw-mode-btn { flex:1; text-align:center; padding:8px 0; border-radius:calc(var(--radius) - 2px); font-size:12.5px; color:var(--muted); cursor:pointer; display:flex; align-items:center; justify-content:center; gap:6px; background:none; border:none; font-family:inherit; }
  .aw-mode-btn.active { background:var(--surface2); color:var(--text); font-weight:500; box-shadow:0 1px 2px rgba(0,0,0,.15); }
  /* !important: some of these elements get an inline display style set by JS
     (e.g. add-bottom-row switches to display:grid once a manual lookup
     completes) which would otherwise outrank this rule and bleed the wrong
     mode's UI through when switching tabs. */
  [data-aw-mode="search"] .aw-only-manual { display:none !important; }
  [data-aw-mode="manual"] .aw-only-search { display:none !important; }
  /* ── Market result (search mode only) — layout comes from .wd-card/.wd-hero
     (shared with Watch Detail's hero card). ── */

  /* Market-snapshot listing rows (#vinted-snapshot) — last row shouldn't
     carry the same bottom breathing room as the ones above it, since the
     card's own padding already closes it out. */
  .aw-listing-row:last-child { padding-bottom: 0 !important; }

  /* Slow pulsing dashed outline (Luke, 2026-10-05: "encourage engagement
     (as seen on the home page)") — same pattern as the Watch-First
     landing page's .wf-searchfield (static/home.css): outline-color's
     opacity fades in/out rather than the outline itself appearing/
     disappearing, so the box's size never jumps. Only ever visible before
     a card's picked — #aw-search-controls is swapped for the chosen-card
     chip once one is. */
  .aw-searchfield { display:flex; align-items:center; gap:8px; height:42px; border:1px solid var(--field-border,var(--border2)); border-radius:var(--radius); background:var(--surface); padding:0 12px; outline:2px dashed var(--brand); outline-offset:3px; animation:aw-searchfield-pulse 2.4s ease-in-out infinite; }
  @keyframes aw-searchfield-pulse {
    0%, 100% { outline-color: color-mix(in srgb, var(--brand) 15%, transparent); }
    50% { outline-color: color-mix(in srgb, var(--brand) 70%, transparent); }
  }
  @media (prefers-reduced-motion: reduce) {
    .aw-searchfield { animation:none; outline-color: color-mix(in srgb, var(--brand) 40%, transparent); }
  }
  /* Manual mode's text inputs match Search's field and the language dropdown: 42px tall (Tom, 2026-10-10). */
  .aw-only-manual .field input[type="text"] { height:42px; padding:0 12px; box-sizing:border-box; }
  .aw-search-row { display:flex; align-items:center; gap:8px; }
  .aw-search-row > .aw-searchfield { flex:1; min-width:0; }
  .aw-lang { position:relative; flex-shrink:0; }
  .aw-lang-btn { display:flex; align-items:center; gap:7px; height:42px; padding:0 10px; background:var(--surface); border:1px solid var(--field-border,var(--border2)); border-radius:var(--radius); color:var(--text); font:inherit; font-size:12.5px; font-weight:600; letter-spacing:.03em; cursor:pointer; }
  .aw-lang-btn:hover, .aw-lang-btn[aria-expanded="true"] { border-color:var(--muted); }
  .aw-lang-btn:focus-visible, .aw-lang-opt:focus-visible { outline:2px solid var(--brand); outline-offset:2px; }
  .aw-lang-btn .chev { color:var(--muted); transition:transform .15s; }
  .aw-lang-btn[aria-expanded="true"] .chev { transform:rotate(180deg); }
  .aw-lang-flag { width:20px; height:20px; border-radius:50%; object-fit:cover; flex-shrink:0; display:block; box-shadow:0 0 0 1px rgba(0,0,0,.18); }
  .aw-lang-menu { position:absolute; top:calc(100% + 6px); right:0; z-index:60; min-width:100%; padding:4px; background:var(--surface); border:1px solid var(--border2); border-radius:var(--radius); box-shadow:0 10px 28px rgba(0,0,0,.28); }
  .aw-lang-menu[hidden] { display:none; }
  .aw-lang-opt { display:flex; align-items:center; gap:8px; width:100%; padding:7px 10px; background:none; border:0; border-radius:calc(var(--radius) - 2px); color:var(--text); font:inherit; font-size:12.5px; font-weight:600; letter-spacing:.03em; cursor:pointer; text-align:left; }
  .aw-lang-opt:hover { background:var(--surface2); }
  .aw-lang-opt[aria-selected="true"] { background:var(--brand-bg); color:var(--brand); }
  .aw-searchfield input { flex:1; background:none; border:none; outline:none; color:var(--text); font-size:13px; font-family:inherit; min-width:0; }
  .aw-searchfield svg { color:var(--muted); flex-shrink:0; }
  .aw-searchfield .clear { cursor:pointer; color:var(--muted); flex-shrink:0; display:flex; }
  /* Luke, 2026-10-23: "leave the full width search field... with the green
     tick selection name and the 'change' put into the field" — once
     locked (awLockSearchField), the attention-seeking pulse stops (nothing
     left to draw attention to) and the field reads as a completed, settled
     state instead of an open input. */
  .aw-searchfield.is-locked { outline-color: color-mix(in srgb, var(--green) 35%, transparent); animation:none; background:var(--green-bg); }
  .aw-searchfield.is-locked input { color:var(--green); font-weight:700; cursor:default; }
  .aw-searchfield-change { flex-shrink:0; font-size:12.5px; font-weight:600; color:var(--muted); cursor:pointer; white-space:nowrap; display:flex; align-items:center; }
  .aw-searchfield-change:hover { color:var(--text); }
  /* Desktop: the "Change" text; mobile swaps to a bare X icon instead
     (see the @media(max-width:500px) override below). */
  .aw-searchfield-change-x { display:none; }
  /* Luke, 2026-10-25: "take the fields out of the card... now they need
     labels (above each field on the left)" — follow-up to the earlier
     "one line, in the style of the 'Search' field" round: Card name/
     Number/Card set each sit inside a plain .field column (label above,
     left-aligned — .field's own base stacking, unchanged) wrapping a
     literal clone of the Search field's own .aw-searchfield markup (icon,
     input, always-visible .clear — see dashboard.html; no new CSS needed
     for any of that, it's the exact same component). The flex-grow ratios
     live on these outer .field columns (dashboard.html inline style), not
     the inner .aw-searchfield — a shared flex-basis:0 with 2:1:3 grow
     weights gives #f-name 1/3, #f-num 1/6, #f-set 1/2 of whatever's left
     after .aw-lang's own fixed natural width and the row's gaps, which is
     exactly "1/2 width less the language button" without needing a
     literal calc(). */
  .aw-manual-fields-row { display:flex; align-items:flex-start; gap:8px; }
  /* Luke, 2026-10-25 (final correction): "border inconsistency between
     tabs is my problem" — the var(--surface2) background tried a couple
     rounds ago (to make the border read more clearly on its own) was
     itself the inconsistency: it made these three fields visually differ
     from the Search tab's own #aw-searchfield, which keeps its default
     var(--surface). Removed entirely — this is the exact same component,
     unmodified, so there's nothing left to diverge.
     Luke, 2026-10-25: "not all three fields need to pulse, just the card
     name one is fine" — back to #f-name only (the "match the tick
     reference" round that made all three pulse is reverted); .aw-manual-field
     suppresses #f-num/#f-set's outline/animation again, same as originally. */
  .aw-manual-field { outline:none; animation:none; }
  .aw-manual-field:focus-within { border-color: var(--brand); }
  /* The language column's own label is a blank spacer (dashboard.html)
     purely to keep .aw-lang's pill vertically aligned with the labeled
     fields beside it on the desktop row — invisible but still occupies
     the same height, rather than .aw-lang sitting 16px higher than its
     neighbors. Collapsed away entirely once stacked (next block down),
     where it'd otherwise read as a stray blank line. */
  .aw-manual-lang-col label { visibility:hidden; }
  /* Luke, 2026-10-25: "on mobile view the language dropdown should always
     be to the right of the card set field" — Card name and Number each
     get their own full-width line (flex-basis:100% forces a wrap before
     them, same trick used elsewhere on this page for "drop this onto its
     own row"), but Card set and the language dropdown stay side by side
     as a pair on the line after, instead of every field — including
     language — stacking into its own full-width row. flex-wrap (not
     flex-direction:column) is what makes a true row/wrap pairing possible
     here; align-items:stretch still makes every field full-bleed within
     whichever line it lands on. */
  /* Luke, 2026-10-25: "at 800px we wrap #f-set and .aw-lang-button to a
     second line[;] at 500px we wrap #f-name and #f-num to separate lines
     also" — two explicit tiers, not one. ≤800px: #f-name/#f-num share a
     full-width line between them (33.333%/16.667% of the full row scales
     up to a 2:1 split once it's just the two of them — their desktop
     percentages summing to exactly 100% here is what leaves zero room for
     #f-set/.aw-lang, pushing both of those onto their own line together,
     same pairing pattern as #f-set/.aw-lang already use on the previous
     line at ≥801px). ≤500px: #f-name/#f-num stop sharing that line too —
     each gets its own (#f-set/.aw-lang's own pairing is untouched, still
     together on whatever line follows). */
  @media (max-width:800px) {
    .aw-manual-fields-row { flex-wrap: wrap; align-items:stretch; }
    /* Explicit percentage basis (not flex-grow off a shared 0 basis) is
       what actually forces the wrap here — with everything starting from
       basis:0, flex-wrap's own hypothetical-size check sees all four
       items as ~0-width and never wraps at all, same nested-flex trap
       hit earlier getting #f-set/.aw-lang to pair on mobile. 66.667%/
       33.333% (the 2:1 ratio scaled up to fill 100% on their own, now
       that it's just the two of them) sum to exactly the row's width,
       leaving zero room for #f-set/.aw-lang, which pushes both of those
       onto their own line together. */
    .aw-manual-name-col { flex: 0 0 calc(66.667% - 4px) !important; min-width: 0; }
    .aw-manual-num-col { flex: 0 0 calc(33.333% - 4px) !important; min-width: 0; }
    .aw-manual-set-col { flex: 1 1 0% !important; min-width: 0; }
    .aw-manual-lang-col { flex: 0 0 auto !important; }
    /* Luke, 2026-10-25: "the language dropdown should always align to the
       bottom edge of the Card set field... it's 19px too high" — this
       display:none (meant for when language used to stack as its own
       full-width row, with a real blank line looking stray) was hiding
       the same invisible label-spacer that keeps .aw-lang's pill bottom-
       aligned with Card set's own label+pill — removing it is exactly
       what pushed .aw-lang 19px (one label line + gap) too high once
       language started pairing beside Card set instead of stacking below
       it. The base visibility:hidden spacer (kept, not duplicated here)
       is correct at every width language pairs with Card set — which is
       now all of them. */
  }
  @media (max-width:500px) {
    .aw-manual-name-col, .aw-manual-num-col { flex: 1 1 100% !important; }
  }
  /* Luke, 2026-10-25: "between ≤800px - 500px the buy and offer alerts
     should be 50%:50% on one line (and only get their own line at
     ≤500px... same with the Include and Exclude Keywords fields" — the
     site-wide @media(max-width:800px) block collapses .form-grid/
     .threshold-row to a single column unconditionally (meant for the
     generic case, e.g. Watch Detail's own narrower layouts), which was
     also flattening Manual mode's Include/Exclude keywords and Buy/Offer
     boxes a full 300px earlier than wanted. Restores the 2-column grid
     for just this 501-800px band — ≤500px still gets the site-wide
     single-column collapse untouched, and ≥801px already renders 2-column
     from each component's own base rule. */
  @media (min-width:501px) and (max-width:800px) {
    #aw-refine-collapse .form-grid, #pane-add .threshold-row { grid-template-columns: 1fr 1fr !important; }
    /* The 2-column grid alone isn't enough — Include/Exclude keywords'
       own .field wrappers each carry an inline grid-column:1/-1 (meant to
       force a full-width row regardless of how many columns the grid
       has), which needs overriding too or they keep spanning both columns
       and stacking exactly as before. */
    #aw-refine-collapse .form-grid > .field { grid-column: auto !important; }
  }
  /* "The language dropdown cannot be changed unless the search field is
     unlocked" — same lock state as the field itself, via awLockSearchField/
     awUnlockSearchField toggling this class on the dropdown's own wrapper.
     Luke, 2026-10-23: "in all circumstances when the language dropdown is
     disabled... remove it's dropdown arrow, centralise the flag and
     acronym, and keep it vivid looking (it's confirming the language so
     should be visible but not changable)" — plain .locked, not scoped to
     #aw-search-lang's id, so any future reuse of this same lock pattern
     elsewhere gets the same treatment automatically. No more opacity dim
     (that read as "disabled/greyed out", not "confirmed"); the chevron
     hides and the button centers its remaining flag+code content now
     that there's no arrow holding the right edge open. */
  .locked.aw-lang, [data-aw-lang].locked { pointer-events:none; }
  .locked.aw-lang .aw-lang-btn, [data-aw-lang].locked .aw-lang-btn { justify-content:center; }
  .locked.aw-lang .chev, [data-aw-lang].locked .chev { display:none; }
  /* Luke, 2026-10-23 (fine-tune): "double" the gap between the search
     field and the cards below it — on top of .aw-grid's own 16px gap. */
  #aw-search-controls { margin-bottom: 16px; }

  .aw-graded-switch { display:flex; align-items:center; gap:8px; margin:10px 2px 0; cursor:pointer; user-select:none; width:fit-content; }
  .aw-graded-switch input { position:absolute; opacity:0; width:0; height:0; }
  .aw-graded-track { position:relative; width:34px; height:19px; border-radius:20px; background:var(--surface2); border:1px solid var(--border2); flex-shrink:0; transition:background .15s, border-color .15s; }
  .aw-graded-thumb { position:absolute; top:1px; left:1px; width:15px; height:15px; border-radius:50%; background:var(--muted); transition:transform .15s, background .15s; }
  .aw-graded-switch input:checked + .aw-graded-track { background:var(--brand-bg); border-color:var(--brand-border); }
  .aw-graded-switch input:checked + .aw-graded-track .aw-graded-thumb { transform:translateX(15px); background:var(--brand); }
  .aw-graded-label { font-size:12px; color:var(--text); }

  /* Graded price, Add Watch only (Luke, 2026-10-09) — sits right under the
     green selected-card chip in step 1. .aw-graded-switch above is reused
     as-is for the toggle itself. Luke: "a single line of options" — company
     codes are always 3 letters and grades are at most 2 digits, so the two
     selects stay narrow/content-sized (not flex:1) and sit on one row with
     the button, instead of each stretching or wrapping to their own line. */
  /* Luke, 2026-10-09: "put those drop downs and the update price button on
     the same row as the toggle" — .aw-slab-row itself is now the single
     flex row (toggle + fields inline together); .aw-slab-msg still gets
     its own line below via flex-basis:100%, since it's a dynamic status
     message that can't usefully share a row with the controls above it. */
  .aw-slab-row { margin-top:10px; padding:10px; display:flex; align-items:center; flex-wrap:wrap; gap:10px; }
  /* Luke, 2026-10-09: the switch carried its own standalone margin-top:10px
     (from when it rendered alone, no row around it) — .aw-slab-row's own
     margin/gap already place it now, so the leftover self-margin was
     nudging it out of vertical center against the selects/button beside it. */
  .aw-slab-row .aw-graded-switch { margin:0; flex-shrink:0; }
  /* Tom/Luke, 2026-10-10: the toggle moved when switched on - align-items:center centred it against the whole fields
     block, which wraps taller on a narrow card, so it dropped. Pinned to the FIRST line instead, at the height of the
     selects (~34px), so it sits in exactly the same place off and on, in Search and Manual mode alike. */
  .aw-slab-row .aw-graded-switch { align-self:flex-start; height:34px; }
  /* "scale the dropdowns to fill the width" — the fields group grows to
     take whatever space the toggle + button don't need, and the two
     selects share that space evenly (flex:1) instead of sitting at a
     fixed content width. */
  .aw-slab-fields { display:flex; align-items:center; gap:8px; flex-wrap:wrap; flex:1 1 160px; min-width:0; }
  .aw-slab-fields select {
    /* Luke, 2026-10-10: "make sure that 3 letters are always clearly
       seen" — flex:1 alone could shrink this past a 3-letter code's own
       content width on a narrow viewport; min-width floors it at enough
       room for "ACE"/"BGS" etc plus the native dropdown arrow. */
    flex:1 1 0; min-width:62px; background:var(--surface2); border:1px solid var(--border2);
    border-radius:var(--radius); padding:7px 8px; color:var(--text);
    font-family:var(--font); font-size:12.5px; outline:none; transition:border-color .15s;
  }
  .aw-slab-fields select:focus { border-color:var(--brand); }
  .aw-slab-btn {
    flex-shrink:0;
    /* border:1px solid transparent (not the select's own border:none)
       matches the selects' own 1px border exactly, so the button's box
       height lines up with theirs — Luke, 2026-10-10: "make sure the
       Update Price button is always the same height as the grading
       drop downs". Transparent keeps the solid red fill looking identical
       to before. */
    background:var(--brand); border:1px solid transparent; color:var(--brand-contrast);
    border-radius:var(--radius); padding:7px 12px; font-family:var(--font);
    font-size:12.5px; font-weight:600; cursor:pointer; transition:opacity .15s; white-space:nowrap;
  }
  .aw-slab-btn:hover { opacity:.85; }
  .aw-slab-btn:disabled { opacity:.5; cursor:default; }
  .aw-slab-msg { font-size:11px; color:var(--muted); flex-basis:100%; }
  .aw-slab-msg:empty { display:none; }

  /* "Find it graded" card (Luke, 2026-10-23, from the redesign mockup) —
     chip pickers instead of <select>s + an "Update Price" button; picking
     both a company and a grade auto-fires the lookup (see
     awSlabPickCompany/awSlabPickGrade/awGetSlabPrice in dashboard.js).
     Scoped to #aw-slab-row specifically — Manual mode's own Condition
     section reuses the plain .aw-slab-fields/.aw-graded-switch classes
     unscoped, as a flex row of real <select>s, untouched. */
  .aw-graded-card-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:14px; padding-right:40px; }
  /* Luke, 2026-10-23 (fine-tune): "move that toggle further into the top
     right of the card" — tucked into #aw-slab-row's own corner (see its
     position:relative in dashboard.html) instead of the title's row. */
  .aw-graded-switch-corner { position:absolute; top:14px; right:14px; margin:0; }
  #aw-slab-row .aw-slab-fields,
  /* Luke, 2026-10-25: same column stacking as #aw-slab-row's own fields —
     without this, .grade-companies and .grade-nums sit side by side in
     one row (the base .aw-slab-fields rule above) and squeeze the grade
     chips into almost no space instead of getting their own full-width
     row underneath the company chips. */
  #grading-options { flex-direction:column; align-items:stretch; gap:10px; }
  /* Luke, 2026-10-23 (fine-tune): "the graded options should be visible,
     just disabled" + "visible, just disabled" until the switch above is
     on — dims the whole fields block and blocks clicks on the chips. */
  .aw-slab-fields.aw-slab-disabled { opacity:.45; pointer-events:none; }
  .grade-companies { display:flex; gap:8px; flex-wrap:wrap; }
  /* Luke, 2026-10-23 (fine-tune 2): "an effect I want for the grading
     values is attached" — real third-party slab labels: a solid
     brand-colored chip per grading company (awSlabCompanyColor in
     dashboard.js assigns each code a color, grey fallback for anything
     not in the map), paired with a small light "grade value" pill for
     7/8/9/10 — "we can make these grading buttons far smaller". Company
     color is always on (identifies the brand at a glance); the active/
     selected state is a bright ring, not a background swap, since the
     background's already spoken for. */
  /* Luke, 2026-10-23: "add slight gradient to those bold colours" — same
     diagonal sheen as .aw-grade-badge-co, driven by the same --co-color
     custom property (both set inline from awSlabCompanyColor() in
     dashboard.js) instead of a flat background. */
  .co-chip { height:30px; padding:0 9px; display:inline-flex; align-items:center; border-radius:6px; border:1px solid transparent; color:#fff; font-family:var(--font); font-size:12px; font-weight:800; letter-spacing:.03em; cursor:pointer; opacity:.6; transition:opacity .15s, box-shadow .15s; background: linear-gradient(135deg, color-mix(in srgb, var(--co-color) 100%, white 18%), var(--co-color) 55%, color-mix(in srgb, var(--co-color) 100%, black 20%)); }
  .co-chip:hover { opacity:.85; }
  .co-chip.active { opacity:1; box-shadow:0 0 0 2px var(--surface), 0 0 0 3.5px var(--brand); }
  .grade-nums { display:flex; gap:6px; }
  .grade-chip {
    flex:1; max-width:40px; height:24px; display:inline-flex; align-items:center; justify-content:center;
    border-radius:5px; background:#f1f2f4; border:1px solid transparent; color:#17181c;
    font-family:var(--font); font-size:12px; font-weight:800; cursor:pointer;
    opacity:.6; transition:opacity .15s, box-shadow .15s;
  }
  .grade-chip:hover { opacity:.85; }
  .grade-chip.active { opacity:1; box-shadow:0 0 0 2px var(--surface), 0 0 0 3.5px var(--brand); }
  /* Luke, 2026-10-24: "the slab buttons and grades can stretch to fill the
     width of #aw-slab-row" — .co-chip has no flex-basis of its own by
     default (sized to its label's content, like any other inline-flex
     button); adding flex:1 here lets it share the row's full width evenly
     with its siblings. .grade-chip already has flex:1 (shared with other
     .grade-chip instances elsewhere that DON'T want this) but was capped
     at max-width:40px — dropped here, scoped to #aw-slab-row specifically
     so the compact look elsewhere (if any) is untouched. */
  #aw-slab-row .co-chip { flex: 1; justify-content: center; }
  #aw-slab-row .grade-chip { max-width: none; }
  /* Luke, 2026-10-25: "the grading buttons should distribute to fill
     width" — same stretch treatment as #aw-slab-row's own chips above,
     applied to Manual mode's #aw-manual-slab-row equivalent. */
  #aw-manual-slab-row .co-chip {
    flex: 1; justify-content: center; min-width: 0; padding: 0 6px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  #aw-manual-slab-row .grade-chip { max-width: none; }
  /* Luke, 2026-10-25: "do not wrap the grading companys... and TAG should
     not be on a line on it's own" — .grade-companies' own base rule
     (flex-wrap:wrap) let the company row spill TAG onto its own second
     line once there's enough companies to not fit at once; nowrap forces
     them to stay on one line and shrink instead. min-width:0 is what
     actually lets a flex item shrink below its own text's min-content
     width (the default refusal to shrink further) — without it nowrap
     alone just overflows the row instead of compressing the chips; the
     ellipsis/ nowrap/overflow trio keeps a squeezed label readable
     (e.g. "BGS…") instead of visibly clipping mid-letter. */
  #aw-manual-slab-row .grade-companies { flex-wrap: nowrap; }
  /* Luke, 2026-10-25: "let's keep the companys top, and the grades bottom
     (now that we are showing more companies)" — this used to switch back
     to a row pairing .grade-companies/.grade-nums side by side below
     688px (fine with only ACE/PSA, cramped now that up to 4 companies can
     be enabled); #aw-slab-row now just keeps the base column stack (see
     the flex-direction:column rule above) at every width, same as Manual
     mode's #grading-options already does. */

  /* Graded price miss recovery card (Luke, 2026-10-10) — sits in place of
     every other step card once PokePulse has no price for the exact
     company+grade tried; see #aw-slab-miss in dashboard.html. Luke:
     "centralise the text" — the whole card reads as a single centered
     message + choice, not a left-aligned form row, so the buttons sit
     at their own content width rather than stretching to fill it. */
  .aw-slab-miss-msg { font-size:13px; color:var(--text-secondary); line-height:1.55; margin-bottom:16px; text-align:center; }
  .aw-slab-miss-actions { display:flex; justify-content:center; gap:10px; flex-wrap:wrap; }
  .aw-slab-miss-btn {
    min-width:160px; padding:11px 16px; border-radius:var(--radius);
    font-family:var(--font); font-size:13px; font-weight:600; cursor:pointer;
    text-align:center; transition:opacity .15s, transform .15s;
  }
  .aw-slab-miss-btn:hover { opacity:.88; transform:translateY(-1px); }
  .aw-slab-miss-btn.secondary { background:none; border:1px solid var(--border2); color:var(--text); }
  .aw-slab-miss-btn.primary { background:var(--brand); border:none; color:var(--brand-contrast); }

  .aw-pick-cap { display:flex; align-items:center; justify-content:space-between; margin:12px 2px 6px; }
  .aw-pick-cap .l { font-size:11px; color:var(--muted); font-weight:500; }
  .aw-pick-cap .r { font-size: 11px; color:var(--dim); }
  /* Luke, 2026-10-09: "Show 4 responses within the list and then scroll" —
     each row is 72px (60px thumb + 6px top/bottom padding) + a 1px
     border-top on every row after the first; 4 rows = 72*4 + 3px. */
  .aw-pick { border:1px solid var(--border2); border-radius:var(--radius); max-height:291px; overflow-y:auto; }
  .aw-prow { display:flex; gap:10px; align-items:center; padding:6px; cursor:pointer; }
  .aw-prow + .aw-prow { border-top:1px solid var(--border); }
  .aw-prow:hover { background:var(--surface); }
  .aw-prow.sel { background:var(--green-bg); }
  .aw-pthumb { height:60px; aspect-ratio:5/7; border-radius:5px; border:1px solid var(--border2); background:var(--surface); display:flex; align-items:center; justify-content:center; flex-shrink:0; overflow:hidden; }
  .aw-pthumb img { width:100%; height:100%; object-fit:cover; }
  .aw-pinfo { flex:1; min-width:0; }
  .aw-pl1 { display:flex; justify-content:space-between; align-items:center; gap:8px; }
  .aw-pname-wrap { display:flex; align-items:center; gap:6px; min-width:0; overflow:hidden; }
  .aw-pname { font-size:13px; font-weight:700; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .aw-pbadge { font-size: 11px; padding:1px 6px; border-radius:20px; white-space:nowrap; flex-shrink:0; font-weight:500; background:var(--surface2); color:var(--muted); }
  .aw-pbadge.grade { background:rgba(91,155,240,.15); color:#5b9bf0; }
  .aw-prt { display:flex; align-items:center; gap:6px; flex-shrink:0; max-width:40%; }
  .aw-pprice { font-size:12.5px; font-weight:500; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
  .aw-ptrend { font-size: 11px; color:var(--green); display:flex; align-items:center; gap:1px; white-space:nowrap; }
  .aw-ptrend.down { color:var(--red); }
  .aw-pl2 { font-size:11px; color:var(--muted); margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .aw-pl3 { font-size: 11px; color:var(--dim); margin-top:1px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .aw-pl3:empty { display:none; }
  .aw-pcheck { flex-shrink:0; color:var(--green); }

  .aw-manual-link { margin-top:10px; border:1px dashed var(--border2); border-radius:var(--radius); padding:10px 12px; display:flex; align-items:center; gap:10px; cursor:pointer; }
  .aw-manual-link:hover { border-color:var(--muted); }
  .aw-manual-link .ic { width:28px; height:28px; border-radius:7px; background:var(--surface); display:flex; align-items:center; justify-content:center; flex-shrink:0; color:var(--muted); }
  .aw-manual-link .t { font-size:12.5px; font-weight:500; color:var(--text); }
  .aw-manual-link .h { font-size: 11px; color:var(--muted); margin-top:1px; }
  .aw-manual-link .chev { margin-left:auto; color:var(--muted); flex-shrink:0; }

  .aw-empty-hint { font-size:11px; color:var(--dim); padding:12px 2px; text-align:center; }
  .aw-selected-chip { display:flex; align-items:center; gap:10px; border:1px solid var(--green-border); background:var(--green-bg); border-radius:var(--radius); padding:9px 11px; margin-top:10px; }
  .aw-selected-chip .txt { flex:1; min-width:0; font-size:12.5px; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .aw-selected-chip .change { font-size:11px; color:var(--green); cursor:pointer; flex-shrink:0; font-weight:500; }

  /* ── Pricing page ── */
  .pricing-wrap { max-width: 1100px; margin: 0 auto; padding: 40px 24px 60px; }
  /* .pane already carries its own 14px side padding on mobile — this
     24px on top of that double-padded the page (Luke, 2026-09-25:
     "doesn't need left and right padding"). Top/bottom untouched. */
  @media (max-width:800px) { .pricing-wrap { padding-left: 0; padding-right: 0; } }
  .pricing-hero { text-align: center; margin-bottom: 40px; }
  .pricing-hero h1 { font-family: var(--font); font-size: 48px; letter-spacing: .03em; line-height: 1; margin-bottom: 10px; }
  /* .pricing-hero is shared across Pricing, Referral, Support and
     (2026-09-24) the affiliate application page — the caps/size override
     is scoped to just these pages' own hero instance. */
  #pane-pricing .pricing-hero h1, #pane-referral .pricing-hero h1, #pane-support .pricing-hero h1, #affiliate-page .pricing-hero h1 { text-transform: uppercase; font-size: 36px; line-height: 1.2; letter-spacing: -.01em; }
  #pane-pricing .pricing-hero p, #pane-referral .pricing-hero p, #pane-support .pricing-hero p, #affiliate-page .pricing-hero p { font-size: 16px; }
  .pricing-hero h1 span { color: var(--brand); }
  .pricing-hero p { color: var(--text-secondary); font-size: 13px; }
  /* Shrink-wraps to the H1's own text width (rather than the H1's own
     full-width block box) so the starburst's `right:` offset below
     tracks the heading's actual visible right edge — Luke, 2026-09-18:
     "tie the starburst to the right edge of the simple transparent
     pricing element." */
  .pricing-hero-title-wrap { position: relative; display: inline-block; }
  .billing-toggle-wrap { position: relative; display: inline-block; margin-top: 20px; }
  .billing-toggle { display: inline-flex; padding: 3px; background: var(--surface2); border: 1px solid var(--border2); border-radius: var(--r-pill); gap: 2px; }
  .billing-toggle-btn { font-family: var(--font); font-size: 12px; font-weight: 500; padding: 7px 18px; border: none; border-radius: var(--r-pill); background: transparent; color: var(--muted); cursor: pointer; transition: background .15s, color .15s; }
  .billing-toggle-btn.active { background: var(--brand); color: var(--brand-contrast); }
  /* First Month % Discount starburst (Luke, 2026-09-18) — tied to the
     right edge of the "Simple, transparent pricing" heading via
     .pricing-hero-title-wrap above, using `right:` so it tracks that
     edge regardless of copy/viewport, rather than a `left:` offset
     tuned against the toggle below. The star itself is an SVG polygon,
     tilted as pure background decoration; the text is a separate,
     unrotated layer on top sized to actually fit inside the star's
     body, rather than a CSS clip-path trying to hide overflow — 3
     lines of copy at a real font size overflowed past the star's
     points with that approach once it wrapped. Hidden until
     _applyFirstMonthStarburst() (dashboard.js) confirms the admin-set
     discount is > 0. Placeholder top/right offsets below — pending
     Luke's own tuned values per breakpoint, same as last time. */
  .fm-starburst { position: absolute; top: 137%; right: -10%; transform: rotate(12deg); width: 126px; height: 130px; z-index: -10; }
  .fm-starburst-svg { position: absolute; inset: 0; width: 100%; height: 100%; fill: var(--brand); transform: rotate(-8deg); filter: drop-shadow(0 2px 6px rgba(0,0,0,.25)); }
  .fm-starburst-text { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; color: var(--brand-contrast); padding: 0 20px; }
  .fm-starburst-pct-line { font-size: 20px; font-weight: 800; line-height: 1; }
  .fm-starburst-off { font-size: 15px; }
  .fm-starburst-line2 { font-size: 10.5px; font-weight: 600; line-height: 1.3; margin-top: 2px; }
  .fm-starburst-line3 { font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; opacity: .85; margin-top: 3px; }
  .plan-price-then { font-size: 11px; color: var(--muted); margin: -2px 0 6px; }
  .pricing-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 48px; }
  @media (max-width: 1000px) { .pricing-grid { grid-template-columns: repeat(2, 1fr); } }
  @media (max-width: 560px) { .pricing-grid { grid-template-columns: 1fr; } }
  .plan-card { background: var(--surface); border: 1px solid var(--border2); border-radius: var(--radius-lg); padding: 32px 24px; display: flex; flex-direction: column; position: relative; }
  @media (max-width:800px) { .plan-card { margin-bottom: 20px; } }
  .plan-card.featured { border-color: var(--brand); box-shadow: 0 0 32px rgba(232,37,26,0.15); }
  /* Luke, 2026-09-18: "when the free tier has its colours border - it
     should be green like the pill" — the Free card's own "Free
     forever" badge is already green (--green-fill), so its featured
     (currently-on-this-plan) border/glow matches that instead of the
     brand red every other tier's featured state uses. */
  #plan-card-free.featured { border-color: var(--green-fill); box-shadow: 0 0 32px var(--green-bg); }
  .plan-badge { position: absolute; top: -12px; left: 50%; transform: translateX(-50%); font-size: 10px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; padding: 3px 10px; border-radius: 20px; white-space: nowrap; }
  .plan-badge.popular { background: var(--brand); color: var(--brand-contrast); }
  .plan-badge.free-badge { background: var(--green-fill); color: #0a2a14; }
  /* Tom, 2026-09-16: "Add a [downgrading on: date] pill to the plan you
     will be moving to after downgrade completes. use the orange we are
     using for the downgrade text in user settings" — same --amber token
     Settings' own "Moving to X on..." line already uses. */
  .plan-badge.downgrading { background: color-mix(in srgb, var(--amber) 12%, var(--bg) 88%); color: var(--amber); border: 1px solid var(--amber-border); }
  .plan-badge.current { background: var(--brand); color: var(--brand-contrast); }
  /* Scheduled-change banner, top of the Pricing pane — Tom, 2026-09-16:
     "I do like the banner though telling you your plan is going to
     change." Same soft-amber family as the downgrading pill above. */
  .pricing-scheduled-banner { display: flex; align-items: center; gap: 14px; background: color-mix(in srgb, var(--amber) 10%, var(--bg) 90%); border: 1px solid var(--amber-border); border-radius: var(--radius-lg); padding: 16px 20px; margin-bottom: 24px; }
  .pricing-scheduled-banner > svg { flex-shrink: 0; width: 26px; height: 26px; color: var(--amber); }
  .custom-plan-card { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; background: var(--surface); border: 1px solid var(--brand); border-radius: var(--radius-lg); padding: 18px 22px; margin-bottom: 24px; }
  .custom-plan-card .cpc-main { flex: 1 1 260px; min-width: 220px; }
  .custom-plan-card .cpc-kicker { font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-secondary); margin-bottom: 4px; }
  .custom-plan-card .cpc-title { font-size: 18px; font-weight: 700; color: var(--text); }
  .custom-plan-card .cpc-sub { font-size: 13px; color: var(--text-secondary); margin-top: 4px; line-height: 1.5; }
  .custom-plan-card .cpc-price { font-size: 20px; font-weight: 700; color: var(--text); white-space: nowrap; }
  .pricing-scheduled-banner-text { flex: 1 1 auto; font-size: 13px; color: var(--text-secondary); line-height: 1.5; min-width: 220px; }
  .pricing-scheduled-banner-text strong { color: var(--amber); }
  .pricing-scheduled-cancel-btn { flex-shrink: 0; background: var(--bg); border: 1px solid var(--border2); color: var(--text); padding: 10px 18px; border-radius: 8px; font-size: 13px; font-weight: 600; font-family: inherit; cursor: pointer; transition: opacity .15s; }
  .pricing-scheduled-cancel-btn:hover { opacity: .8; }
  @media (max-width: 560px) { .pricing-scheduled-banner { flex-wrap: wrap; } .pricing-scheduled-cancel-btn { width: 100%; } }
  .plan-name { font-family: var(--font); font-size: 28px; font-weight: 700; letter-spacing: .04em; margin-bottom: 4px; }
  .plan-tagline { font-size: 13px; color: var(--text-secondary); margin-bottom: 16px; line-height: 1.5; min-height: 34px; }
  .plan-price { margin-bottom: 6px; white-space: nowrap; }
  .plan-price .amount { font-size: 34px; font-weight: 700; line-height: 1; }
  .plan-price .period { font-size: 13px; color: var(--muted); }
  .plan-save-badge:empty { display: none; }
  .plan-save-badge { display: inline-block; font-size: 11px; font-weight: 700; color: var(--green); background: var(--green-bg); padding: 2px 7px; border-radius: 20px; margin-left: 6px; vertical-align: middle; }
  .plan-sub { font-size: 11px; color: var(--muted); margin-bottom: 20px; }
  .plan-divider { border: none; border-top: 1px solid var(--border); margin-bottom: 16px; }
  .plan-features { list-style: none; flex: 1; margin-bottom: 24px; display: flex; flex-direction: column; gap: 9px; }
  .plan-features li { font-size: 13px; display: flex; align-items: flex-start; gap: 8px; line-height: 1.4; }
  .plan-features li .chk { color: var(--green); flex-shrink: 0; margin-top: 1px; }
  .plan-features li em { color: var(--muted); font-style: normal; }
  .plan-cta { display: block; width: 100%; text-align: center; padding: 12px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; transition: opacity .15s; font-family: inherit; }
  .plan-cta:hover { opacity: .85; }
  .plan-cta.primary { background: var(--brand); color: var(--brand-contrast); }
  .plan-cta.secondary { background: transparent; border: 1px solid var(--border2); color: var(--text); }
  .plan-cta:disabled { background: var(--surface2); color: var(--dim); cursor: not-allowed; opacity: 1; }
  .plan-cta:disabled:hover { opacity: 1; }
  .pricing-section-title { font-family: var(--font); font-size: 28px; letter-spacing: .04em; margin-bottom: 16px; }
  /* Luke, 2026-09-25: "'chasecard credits' and 'anytime purchases' seem
     to align left rather than center" on mobile — these are centered via
     flex (justify-content:center) with an icon/hairline beside the text,
     which only visually centers when the row's content is narrower than
     its container; at 28px uppercase, the longer headings wrapped onto
     two lines and filled the full width, leaving nothing for
     justify-content to center against (icon pinned to the left edge,
     text filling the rest). Shrinking the font on mobile keeps every
     heading here to one line, so the existing centering actually shows. */
  @media (max-width:800px) { .pricing-section-title { font-size: 20px; } }
  .pricing-section-sub { font-size: 12px; color: var(--muted); margin-bottom: 24px; }
  /* Additional Watches / Watch Boosts only — not the shared
     .pricing-section-title base (also used by FAQ and Referral's own
     headings, untouched here). Mirrors home.css's .hiw-heading cue:
     uppercase + bold. */
  .pst-feature { font-weight: 700; text-transform: uppercase; }
  .info-card { background: var(--surface); border: 1px solid var(--border2); border-radius: var(--radius-lg); padding: 18px 20px; font-size: 12.5px; color: var(--text-secondary); line-height: 1.6; margin-bottom: 48px; }

  /* Group headings for the Collectors/Sellers tier split (Luke,
     2026-09-18) — a hairline either side of the text, using the exact
     same .pricing-section-title.pst-feature type treatment "Additional
     Watches"/"Watch Boosts" already use below (don't invert established
     heading/uppercase conventions for one-off text). */
  /* Bigger bottom margin than .pricing-section-title's own 16px default
     (Luke, 2026-09-18: "a more sizable margin at the bottom") — these
     two sit right above a tier grid/card, not a paragraph, so need more
     breathing room than the FAQ-style headings that default provides. */
  .pricing-group-heading { display: flex; align-items: center; justify-content: center; gap: 18px; margin-bottom: 40px; }
  .pricing-group-heading .line { flex: 1; max-width: 160px; height: 1px; background: var(--border2); }

  /* Tycoon — coming soon tier for Sellers/Vendors/Shop Owners. Luke:
     "We have a style set on the existing tier cards... use that... just
     a colour switch." Same .plan-card markup/structure as Free/Starter/
     Pro/Elite, full-width instead of a grid column, purple (--boost,
     already used for Watch Boosts elsewhere on this page) instead of
     the brand red — same idea as .plan-card.featured's own red glow,
     just a different accent and not confined to the 4-up grid. */
  .plan-card.tycoon { margin-bottom: 48px; border-color: var(--boost-border); box-shadow: 0 0 32px var(--boost-bg); }
  .plan-card.tycoon .plan-name { color: var(--boost); }
  /* Left (identity/price) + right (checklist) inner sections — Luke:
     "The Tycoon card can have a right and left inner section" — a plain
     vertical stack looked too sparse once the card went full-width. */
  .tycoon-cols { display: flex; gap: 44px; align-items: center; }
  .tycoon-col-left { flex: 1 1 300px; }
  .tycoon-col-right { flex: 1 1 320px; border-left: 1px solid var(--border); padding-left: 44px; }
  .tycoon-col-right .plan-features { margin-bottom: 0; }
  @media (max-width: 700px) {
    .tycoon-cols { flex-direction: column; align-items: stretch; gap: 24px; }
    .tycoon-col-left, .tycoon-col-right { flex-basis: auto; }
    .tycoon-col-right { border-left: none; border-top: 1px solid var(--border); padding-left: 0; padding-top: 24px; }
  }
  .tycoon-badge { background: var(--boost); color: #fff; }
  .boosts-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 48px; }
  @media (max-width: 700px) { .boosts-grid { grid-template-columns: repeat(2, 1fr); } }
  .boost-step { background: var(--surface); border: 1px solid var(--border2); border-radius: var(--radius); padding: 16px; text-align: center; }
  .boost-step .boost-speed { font-family: var(--font); font-size: 24px; font-weight: 700; color: var(--blue); }
  .boost-step .boost-label { font-size: 11px; color: var(--muted); margin-top: 4px; }
  /* Same 4-stop blue->purple scale used on the Watch Detail page's own
     check-frequency tiers (renderDetail()'s tierColors) — color-mix,
     not opacity, so this stays theme-aware and actually matches that
     scale's colors rather than just fading one blue. */
  .boost-step.step-1 .boost-speed { color: var(--blue); }
  .boost-step.step-2 .boost-speed { color: color-mix(in srgb, var(--blue) 67%, var(--boost-deep) 33%); }
  .boost-step.step-3 .boost-speed { color: color-mix(in srgb, var(--blue) 33%, var(--boost-deep) 67%); }
  .boost-step.step-4 .boost-speed { color: var(--boost-deep); }
  .boost-pill { display: inline-flex; align-items: center; background: var(--blue-bg); border: 1px solid var(--blue-border); border-radius: var(--r-pill); color: var(--blue); padding: 4px 9px; margin-top: 10px; }
  .pricing-faq { display: flex; flex-direction: column; gap: 0; }
  .faq-item { border-bottom: 1px solid var(--border); }
  .faq-q { display: flex; justify-content: space-between; align-items: center; padding: 14px 0; cursor: pointer; font-size: 13px; font-weight: 700; gap: 16px; }
  .faq-q svg { flex-shrink: 0; transition: transform .2s; color: var(--muted); }
  .faq-q.open svg { transform: rotate(180deg); }
  .faq-a { font-size: 12px; color: var(--muted); line-height: 1.7; display: none; padding-bottom: 14px; }
  .faq-a.open { display: block; }
  .pricing-note { text-align: center; font-size: 11px; color: var(--muted); margin-top: 48px; line-height: 1.8; }

  /* Wallet "Tickets" section (added 2026-09-07) — rows reuse .faq-item/
     .faq-q/.faq-a above; .wallet-ticket-headrow is the flex row inside
     .faq-q's own single content slot (main title/sub on the left, draw
     info on the right), keeping .faq-q itself at its normal 2-child
     shape (this row + the chevron). Wrapped in .wallet-history-card
     (2026-09-08, Tom: "put the ticket rows... in a card, similar to the
     purchase history") — that card has no padding of its own (rows
     carry it, see .wallet-history-item), so .faq-q/.faq-a need the same
     horizontal inset here that they don't need on a plain page
     background; the last row's border is also dropped so the card's
     own bottom edge is the only line there, same as .wallet-history-
     item:last-child does. */
  .wallet-history-card .faq-q { padding: 14px 16px; }
  .wallet-history-card .faq-a { padding: 0 16px 14px; }
  .wallet-history-card .faq-item:last-child { border-bottom: none; }
  .wallet-ticket-headrow { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex: 1; min-width: 0; }
  .wallet-ticket-main { min-width: 0; }
  /* Luke, 2026-09-15: both the title and the ticket-count/date line need
     to stay on one line each in the collapsed row (previously wrapped
     and pushed the row's height around) — the status pill on the right
     now carries the Live/Drawn/Winner state instead of a wrapping title
     tag (see _walletTicketStatusPillHtml in dashboard.js). */
  .wallet-ticket-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .wallet-ticket-sub { margin-top: 3px; display: flex; align-items: center; gap: 7px; white-space: nowrap; overflow: hidden; }
  .wallet-ticket-count { display: flex; align-items: center; gap: 4px; color: var(--red); flex-shrink: 0; }
  .wallet-ticket-count .ic { display: flex; }
  /* Luke, 2026-09-15: "the ticket icon should be red" — needs an
     explicit color here, not just inherited from .wallet-ticket-count,
     because .faq-q svg (this row reuses the FAQ-accordion markup) sets
     its own explicit grey on every svg inside it, and an explicit rule
     always beats inheriting from an ancestor regardless of specificity. */
  .wallet-ticket-count .ic svg { width: 12px; height: 12px; color: var(--red); }
  .wallet-ticket-count b { color: var(--red); font-weight: 700; }
  .wallet-ticket-dates { color: var(--muted); overflow: hidden; text-overflow: ellipsis; }
  .wallet-ticket-drawinfo { font-size: 11px; color: var(--muted); margin-top: 10px; }
  .wallet-ticket-chips { display: flex; flex-wrap: wrap; gap: 8px; }
  .wallet-ticket-chip { font-size: 11px; font-weight: 600; color: var(--text); background: var(--surface2); border: 1px solid var(--border2); border-radius: var(--r-pill); padding: 5px 10px; }
  .wallet-ticket-chip.won { color: var(--green); background: color-mix(in srgb, var(--green) 12%, transparent); border-color: color-mix(in srgb, var(--green) 40%, transparent); }
  /* "The list of winners and winning ticket numbers if they have been
     drawn" (Luke, 2026-09-15), inside the expanded row — mirrors the
     Reveal modal's .reveal-row shape (prize / winner identity / ticket
     number) at a smaller, inline scale. */
  .wallet-ticket-winners-label { font-size: 10.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin-top: 14px; margin-bottom: 6px; }
  .wallet-ticket-winners { display: flex; flex-direction: column; gap: 6px; }
  /* Luke, 2026-09-15: "we have a trail of dashes that lins the prize
     title to the winner... hard to follow the detail across the page
     when it's a wide desktop page" — the old .prize{flex:1} pushed the
     winner/ticket to the far right edge of a wide card, stranding it
     away from the prize name it belongs to. Keeping prize + winner +
     ticket as one compact, left-aligned inline group (wrapping onto a
     second line if it doesn't fit) keeps the association obvious at
     any width instead of spreading it across the row. */
  .wallet-ticket-winner-row { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 6px; font-size: 12px; }
  .wallet-ticket-winner-row .prize { color: var(--text); font-weight: 600; }
  .wallet-ticket-winner-row .prize::after { content: ":"; }
  .wallet-ticket-winner-row .winner-info { color: var(--muted); white-space: nowrap; }
  .wallet-ticket-winner-row .winner-info .handle { color: var(--dim); }
  .wallet-ticket-winner-row.you .winner-info { color: var(--green); font-weight: 600; }
  .wallet-ticket-winner-row .ticket { color: var(--muted); font-variant-numeric: tabular-nums; }
  .wallet-ticket-winner-row.you .ticket { color: var(--green); }

  /* ── Refer a friend ── */
  /* Referral cards use the site's neutral card + brand-red accent, like
     Pricing/Wallet — not the purple Boost palette (Luke, 2026-09-19). */
  .ref-code-card { background: var(--surface); border: 1px solid var(--border2); border-radius: var(--radius-lg); padding: 28px; text-align: center; margin-bottom: 24px; }
  .ref-code-label { font-size: 14px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); margin-bottom: 10px; }
  .ref-code-value { font-family: var(--font); font-size: 40px; font-weight: 700; line-height: 1.08; letter-spacing: .12em; color: var(--text); margin-bottom: 18px; }
  /* Referral section titles reuse Pricing's .pricing-group-heading (hairline
     either side, 18px gap, 40px below). */
  .ref-note-card { background: var(--surface); border: 1px solid var(--border2); border-radius: var(--radius-lg); padding: 18px 22px; text-align: center; font-size: 12px; line-height: 1.7; color: var(--muted); margin-bottom: 24px; }
  #ref-affiliate-card .submit-btn { width: auto; margin-top: 4px; }
  .ref-code-card p { font-size: 12px; line-height: 1.6; color: var(--muted); }
  .ref-code-card .ref-card-value { font-size: 26px; font-weight: 700; line-height: 1.1; letter-spacing: -.01em; margin-bottom: 4px; }
  .ref-code-actions { display: flex; gap: 8px; max-width: 420px; margin: 0 auto; }
  .ref-code-link { flex: 1; min-width: 0; background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius); padding: 9px 12px; font-size: 12px; color: var(--muted); text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ref-code-copy { flex-shrink: 0; padding: 9px 16px; background: var(--brand); border: none; border-radius: var(--radius); color: var(--brand-contrast); font-size: 13px; font-weight: 500; cursor: pointer; transition: background .15s; font-family: inherit; }
  /* Copy link: text on desktop, Lucide copy icon on mobile (Luke, 2026-09-19). */
  #ref-code-copy-btn .ref-copy-icon { display: none; width: 16px; height: 16px; }
  @media (max-width: 800px) {
    #ref-code-copy-btn { display: inline-flex; align-items: center; justify-content: center; padding: 9px 12px; }
    #ref-code-copy-btn .ref-copy-label { display: none; }
    #ref-code-copy-btn .ic-copy { display: block; }
    #ref-code-copy-btn.is-copied .ic-copy { display: none; }
    #ref-code-copy-btn.is-copied .ic-check { display: block; }
  }
  .ref-code-copy:hover { background: var(--brand-hover); }
  .ref-benefits-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 24px; }
  .ref-benefits-grid.two-col { grid-template-columns: repeat(2, 1fr); }
  /* Affiliate view adds a 4th card (subscription renewals, split out from
     the 2-tier "recurring" card — Tom, 2026-09-24) — non-affiliates never
     see this modifier. */
  .ref-benefits-grid.four-col { grid-template-columns: repeat(4, 1fr); }
  @media (max-width: 900px) { .ref-benefits-grid.four-col { grid-template-columns: repeat(2, 1fr); } }
  @media (max-width: 800px) { .ref-benefits-grid, .ref-benefits-grid.two-col { grid-template-columns: 1fr; } }
  @media (max-width: 500px) { .ref-benefits-grid.four-col { grid-template-columns: 1fr; } }
  .ref-benefit-card { background: var(--surface); border: 1px solid var(--border2); border-radius: var(--radius-lg); padding: 20px; }
  .ref-benefit-icon { width: 38px; height: 38px; border-radius: 10px; display: flex; align-items: center; justify-content: center; margin-bottom: 14px; background: var(--brand-bg); color: var(--brand); }
  .ref-benefit-title { font-size: 14px; font-weight: 700; color: var(--text); margin-bottom: 6px; }
  .ref-benefit-desc { font-size: 12px; color: var(--text-secondary); line-height: 1.6; }

  .ref-summary-tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 24px; }   /* 24px like every other gap between referral cards; 3 tiles since 2026-10-01 (clicks/registers/credits) */
  @media (max-width: 560px) { .ref-summary-tiles { grid-template-columns: 1fr; } }
  .ref-summary-tile { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 16px 18px; }
  .ref-summary-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; margin-bottom: 6px; }
  .ref-summary-val { font-size: 24px; font-weight: 600; color: var(--text); line-height: 1; }
  .ref-summary-val.green { color: var(--green); }
  /* No extra 60px under the referrals table — the footer's own top margin is enough (Luke, 2026-09-19). */
  #pane-referral .pricing-wrap { padding-bottom: 0; }
  .ref-table-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 4px 20px 20px 20px; overflow-x: auto; }
  .ref-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
  .ref-table th { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; text-align: left; padding: 12px 8px; border-bottom: 1px solid var(--border2); font-weight: 700; white-space: nowrap; }
  .ref-table td { padding: 12px 8px; border-bottom: 1px solid var(--border); vertical-align: middle; color: var(--text); }
  .ref-table tr:last-child td { border-bottom: none; }
  .ref-table tr:hover td { background: var(--surface2); }
  .ref-table td.ref-credits { color: var(--green); font-weight: 600; text-align: right; }
  .ref-table th:last-child, .ref-table td.ref-credits { text-align: right; }
  /* Phones (Luke, 2026-09-19): three table columns don't fit at 360px (the
     rewards column got cut off / scrolled), so each referral becomes a two-line
     row — username + rewards on the first line, date underneath — and the
     column headings go. The expandable detail row keeps working as before. */
  @media (max-width: 600px) {
    .ref-table-wrap { padding: 2px 14px 20px 14px; }
    .ref-table, .ref-table tbody { display: block; }
    .ref-table thead { display: none; }
    .ref-table tr[id^="ref-row-"] { display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name rewards" "date rewards"; column-gap: 12px; row-gap: 2px; align-items: center; padding: 12px 0; border-bottom: 1px solid var(--border); }
    .ref-table tr[id^="ref-row-"] td { display: block; padding: 0; border: none; background: none; }
    .ref-table tr[id^="ref-row-"] td:nth-child(1) { grid-area: date; font-size: 11px; color: var(--muted); }
    .ref-table tr[id^="ref-row-"] td:nth-child(2) { grid-area: name; font-size: 13px; font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
    .ref-table tr[id^="ref-row-"] td:nth-child(3) { grid-area: rewards; justify-self: end; }
    .ref-table tr[id^="ref-detail-"] td { display: block; margin-bottom: 10px; }
    .ref-table tr:last-child td { border-bottom: none; }
  }

  ::-webkit-scrollbar { width: 4px; }
  ::-webkit-scrollbar-track { background: transparent; }
  ::-webkit-scrollbar-thumb { background: var(--dim); border-radius: 2px; }

  /* BETA corner ribbon — pinned to the top-right corner of the viewport,
     independent of header layout. Shared identical block copy-pasted
     across every page (single-file monoliths, no shared stylesheet). */
  .beta-ribbon { position: fixed; top: 0; left: 0; width: 130px; height: 130px; overflow: hidden; z-index: 1000; pointer-events: none; }
  .beta-ribbon span {
    position: absolute; top: 24px; left: -34px; width: 170px; padding: 5px 0;
    background: #e8251a; color: #fff; font-weight: 700; font-size: 12px;
    letter-spacing: .1em; text-align: center; text-transform: uppercase;
    transform: rotate(-45deg); box-shadow: 0 2px 8px rgba(0,0,0,.35);
  }
