/* [W9-M21D] Consumer webview SPA styles. English source convention (§2.9 — no Korean literals).
   Adapted from docs/design/시안_커뮤니티웹뷰_M21_v0.html, hardened for the a11y baseline (§2.6):
   body >=14px / captions >=12px, touch targets >=44px, muted/faint bumped for AA contrast on the
   dark surfaces, single <h1> (sr-only), no horizontal overflow. */

/* [AC-18 · v2A] Deep Navy & Gold retheme (§6 C1 — EXPLICIT REPLACEMENT of the W9-M21E black &
   burgundy values). The rule that governed that retheme governs this one: variable NAMES are
   unchanged, only the values move, and there is no var() sweep and no rename. Signal colors
   (playboost/honest/danger) and brand point colors (boost/warpboost) are NOT touched at all — the
   accent moved house, the meanings did not.

   ★ WHY THE ACCENT IS GOLD AND NOT A BLUE. The v2A design sells a benefit, and every surface that
     states one (balance, reward, ranking prize, the exchange CTA) was ALREADY gold in this file via
     --boost. Making the accent a second, competing hue would have put two "look here" colours on the
     same card. Gold on navy also measures better than the burgundy it replaces: --accent-text on
     --surface is 11.8:1 (was 4.9:1) and the ink on a gold fill is 11.9:1 (white on burgundy was
     6.5:1). --accent and --boost carry the same value today and remain SEPARATE variables, because
     they answer different questions and a future divergence must not need a var() sweep.

   ★ --on-accent IS NEW AND IT IS LOAD-BEARING. A gold fill cannot carry white text (1.7:1). Every
     rule that used to hand-write `color:#fff` over the burgundy gradient now names this ink instead,
     so "what colour is legible on the accent" has ONE home and cannot drift per component. #fff
     survives only where it sits on a dark scrim (.attach-rm / .viewer-close), which is a different
     proposition and keeps its own literal.

   ★ --info IS NEW because the accent stopped being a hue of its own. `.badge.info` used to be
     burgundy and `.badge.honest` amber — two distinguishable tones. With a gold accent both would
     have become the same badge, i.e. the signal would have been silently deleted; the blue is the
     v2A design's own informational tone.

   ★ --muted / --faint are NOT the mockup's values. The mockup's #949db8 / #5d6884 measure 3.1:1 on
     --surface, and axe (spa-a11y, wcag2aa) walks these screens. They are lifted to the same AA
     posture the burgundy palette held: --muted 8.3:1, --faint 4.6:1 on the WORST surface it lands on
     (--surface-3), 6.2:1 on the tab bar. */
:root{
  --bg:#05070d;         /* page ground behind the frame */
  --frame-bg:#0a0e18;   /* the phone frame itself */
  --surface:#121a2c;    /* card */
  --surface-2:#1a2440;  /* input / secondary surface */
  --surface-3:#1e2946;  /* panel2 lightened — contrast for disabled/some surfaces */
  --border:#232f4e;
  --border-soft:#1c2740;
  --text:#edf1fb;
  --muted:#aab4ca;      /* AA on the navy surfaces */
  --faint:#8892ab;      /* AA on the navy surfaces (>=4.5:1 on --surface-3, the worst of them) */
  --accent:#ffc233;     /* primary gold */
  --accent-text:#ffce5c;/* accent used as text (tabs/links) — lighter gold */
  --accent-2:#ff8f4f;   /* gradient pair — amber to orange */
  --on-accent:#0a0e18;  /* ink ON an accent fill (the frame navy) — never #fff, see above */
  --info:#7fa3ff;       /* informational tone, kept apart from the gold accent (AA on its own tint) */
  --boost:#ffc233;
  --playboost:#3ddc84;
  --warpboost:#c3a6ff;
  /* [W18-SKIN · sian v2A] the violet axis — the accent that breaks the gold monotony. Taken from the
     mockup, one stop up its own violet ramp: the mockup leads with a darker violet whose literal is
     byte-identical to the RETIRED v0 purple, and m21e bans that literal by value because it cannot
     tell «the old purple came back» from «the new mockup happens to reuse it». This comment does not
     spell that literal either — a comment that explains a rule must not trip the rule s checker. */
  --violet:#b18cff;
  --violet-ink:#c9b8ff;   /* violet used as TEXT on navy — the lighter stop clears AA */
  --violet-tint:rgba(177,140,255,.16);
  --violet-line:rgba(177,140,255,.42);
  --info-2:#5b8cff;       /* the mockup's blue, paired with violet in gradients */
  --honest:#f7c65a;
  --danger:#ff8686;
}

*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{height:100%}
body{
  background:var(--bg);
  background-image:
    radial-gradient(1100px 520px at 50% -8%, rgba(255,194,51,.10), transparent 62%),
    radial-gradient(800px 460px at 88% 108%, rgba(177,140,255,.14), transparent 60%);
  color:var(--text);
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Apple SD Gothic Neo","Malgun Gothic",sans-serif;
  font-size:15px;
  line-height:1.55;
  -webkit-font-smoothing:antialiased;
}
button{font-family:inherit;color:inherit;background:none;border:none;cursor:pointer}
input,textarea,select{font-family:inherit}

.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ── phone frame ─────────────────────────────────────────── */
#app{
  position:relative;
  /* [UX-HOTFIX] height, not min-height. min-height is a floor, so nothing capped the frame: #app grew
     to its content and the DOCUMENT scrolled, which carried the absolutely-positioned tabbar off the
     bottom of the screen (measured 390x420: #app 694 tall, tabbar top 631, all four tabs unreachable).
     A definite height is also what lets .stage's flex:1 + min-height:0 actually shrink and scroll. */
  max-width:480px;margin:0 auto;height:100dvh;
  background:var(--frame-bg);
  box-shadow:0 0 0 1px var(--border),0 0 70px rgba(255,194,51,.10);
  display:flex;flex-direction:column;overflow:hidden;
}

/* ── app bar ─────────────────────────────────────────────── */
.appbar{
  flex:none;display:flex;align-items:center;gap:10px;
  /* [user report 2026-07-25 · CS 0a94cbb1 #10] "buttons are hidden behind the status bar".
     Measured true: this webview paints under the status bar inside the host app, and the app bar
     was the one top-anchored element without a top inset. `.fatal-close` in this same file already
     used calc(12px + env(safe-area-inset-top)) — a SIBLING defect: someone knew the inset was needed
     and fixed exactly one place. The bottom axis (.tabbar/.fab/.toast) all carry inset-bottom and
     were verified clean; the top axis leaked here alone.
     env() resolves to 0 where there is no inset, so this changes nothing on a non-fullscreen host. */
  padding:calc(10px + env(safe-area-inset-top)) 12px 10px;min-height:56px;
  background:linear-gradient(180deg,rgba(255,194,51,.10),transparent 90%);
  border-bottom:1px solid var(--border-soft);
}
.appbar-back{
  width:44px;height:44px;border-radius:12px;flex:none;display:none;
  align-items:center;justify-content:center;font-size:22px;line-height:1;
  background:var(--surface-2);border:1px solid var(--border);color:var(--text);
}
.appbar.has-back .appbar-back{display:flex}
.appbar-titles{flex:1;min-width:0}
.appbar-t1{font-size:16px;font-weight:800;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.appbar-t2{font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.appbar-t2:empty{display:none}
/* [2026-07-31] The titles slot GROWS even when it is empty (no `flex:0 1 8px` collapse here any
   more). That collapse existed to hand the width to the app-bar point chips, which sat between the
   titles and the actions; with the chips removed, every remaining child is flex:none, so a collapsed
   titles slot would pack the notification/profile/close buttons against the LEFT edge with the whole
   right half of the bar empty. Empty-and-growing is what holds them where they have always been. */
.appbar.empty-title .appbar-t1{display:none}
.appbar-close{position:relative;z-index:3;flex:none;min-width:58px;height:44px;padding:0 12px;border-radius:12px;font-size:13px;font-weight:900;color:var(--on-accent);background:linear-gradient(90deg,var(--accent),var(--accent-2));border:1px solid rgba(255,206,92,.32);box-shadow:0 4px 14px rgba(0,0,0,.24)}
.appbar-close:active{filter:brightness(1.12)}
@media (max-width:380px){
  .appbar{gap:5px;padding-inline:8px}
  .appbar-close{min-width:52px;padding-inline:9px}
}

/* ── stage / screens ─────────────────────────────────────── */
.stage{
  /* [UX-HOTFIX] min-height:0 is load-bearing. A column flex item defaults to min-height:auto, which
     refuses to shrink below its content — so .stage sized to its content (measured 390x420: #app 694
     tall in a 420 viewport) and overflow-y:auto never engaged. The DOCUMENT scrolled instead, which
     put .tabbar (absolute, bottom:0 on #app) at top 631 — off-screen and unreachable whenever the
     keyboard halves the viewport. With this, .stage is the scrollport it was always written to be:
     the frame stays put, the tabbar stays reachable, and sticky children can pin. */
  flex:1;min-height:0;position:relative;overflow-y:auto;overscroll-behavior:contain;
  padding:14px 14px calc(96px + env(safe-area-inset-bottom));
  outline:none;
}
.stage>div{animation:fade .18s ease}
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.stage>div{animation:none}}

/* ── common ──────────────────────────────────────────────── */
.card{background:var(--surface);border:1px solid var(--border-soft);border-radius:14px;padding:14px;margin-bottom:12px}
.sec-title{font-size:13px;font-weight:800;color:var(--muted);letter-spacing:.05em;margin:18px 2px 8px}
.footnote{font-size:12px;color:var(--faint);line-height:1.6;border-left:2px solid var(--border);padding:2px 0 2px 9px;margin:12px 2px 0}
/* [W9-M21F] A note that names a destination, with the way there next to it. Wraps at narrow widths so
   the 44px target never squeezes the sentence off screen; existing variables only (no new tokens). */
/* [UX-HOTFIX] bottom margin is no longer 0: the list screen moved this row ABOVE the feed, where a 0
   gap butted it against the first post card. It was the last row before, so only the top mattered. */
.badge{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:700;border-radius:999px;padding:3px 9px;border:1px solid;line-height:1.4;white-space:nowrap}
.badge.honest{color:var(--honest);border-color:rgba(247,198,90,.45);background:rgba(247,198,90,.08)}
/* `info` is the one badge tone that must NOT be gold: with the accent gold it would be the amber
   `honest` badge wearing a second name, and the two are used side by side. */
.badge.info{color:var(--info);border-color:rgba(127,163,255,.42);background:rgba(127,163,255,.10)}
.badge.live{color:var(--playboost);border-color:rgba(61,220,132,.4);background:rgba(61,220,132,.08)}
/* [OWNER 2026-07-24] Server-issued operator identity. The gold shield + double ring + glow are one
   inseparable mark, not a text treatment: a user whose visible nickname contains "GM" still renders
   through `.who` and cannot acquire this class. The wrapper owns the accessible name; its decorative
   children are hidden from assistive tech. */
.gm-badge{display:inline-flex;align-items:center;gap:4px;flex:none;min-height:24px;padding:2px 7px 2px 4px;border-radius:999px;color:#1a1204;background:linear-gradient(135deg,#fff0ae 0%,var(--boost) 52%,#e39b10 100%);border:1px solid #ffe08a;box-shadow:0 0 0 1px rgba(255,194,51,.28),0 0 12px rgba(255,194,51,.32);font-size:12px;font-weight:900;line-height:1;letter-spacing:.07em;white-space:nowrap}
.gm-shield{display:inline-flex;align-items:center;justify-content:center;width:16px;height:18px;border-radius:5px 5px 7px 7px;color:var(--boost);background:#241604;border:1px solid rgba(36,22,4,.72);font-size:12px;line-height:1}
.gm-text{transform:translateY(.2px)}
.link-btn{display:inline-block;margin-top:16px;font-size:13px;color:var(--faint);border:1px solid var(--border);border-radius:9px;padding:8px 12px;min-height:44px}

/* ── gate (fail-closed honest surface) ───────────────────── */
.gate{display:flex;flex-direction:column;align-items:center;text-align:center;gap:8px;padding:34px 18px;background:var(--surface);border:1px solid var(--border-soft);border-radius:16px;margin:10px 0}
.gate-ic{width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:20px;color:var(--honest);background:rgba(247,198,90,.1);border:1px solid rgba(247,198,90,.35)}
.gate-title{font-size:16px;font-weight:800}
.gate-body{font-size:13.5px;color:var(--muted);max-width:30ch}
.gate-code{font-size:12px;color:var(--faint);font-variant-numeric:tabular-nums}
.gate-badges{display:flex;justify-content:center;margin-top:12px}
.fatal{position:relative;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:76px 24px 24px}
.fatal .gate{width:100%;max-width:360px}
.fatal-close{position:absolute;z-index:3;top:calc(12px + env(safe-area-inset-top));right:14px;min-width:72px;height:48px;padding:0 16px;border-radius:14px;font-size:14px;font-weight:900;color:var(--on-accent);background:linear-gradient(90deg,var(--accent),var(--accent-2));border:1px solid rgba(255,206,92,.32);box-shadow:0 6px 18px rgba(0,0,0,.3)}

/* ── home: point chips ───────────────────────────────────── */
/* [W9-UX3] Compacted, NOT reduced: the three .pchip nodes and every line inside them are still here
   (the chip count is the SPA's boot signal across the e2e suite, and dropping the sub line would be
   dropping the sentence that says which points work where). What changed is only the layout: the
   icon moved onto the name's row instead of owning a row of its own, and the paddings / line-heights
   tightened. Every own-text element stays >=12px (a11y baseline §2.6 — the audit walks computed
   font sizes, and .pchip-ic's emoji IS own text). */
.points-bar{display:flex;gap:6px;margin-bottom:10px}
.pchip{flex:1;min-width:0;border-radius:12px;padding:8px 8px 8px 10px;background:var(--surface);border:1px solid var(--border-soft);position:relative;overflow:hidden;display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:5px;row-gap:1px;align-items:center}
.pchip::before{content:"";position:absolute;inset:0 auto 0 0;width:3px;background:var(--pc)}
.pchip.boost{--pc:var(--boost)}.pchip.play{--pc:var(--playboost)}.pchip.warp{--pc:var(--warpboost)}
.pchip-ic{width:20px;height:20px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;background:color-mix(in srgb,var(--pc) 16%,transparent);border:1px solid color-mix(in srgb,var(--pc) 40%,transparent)}
.pchip-nm{font-size:12px;font-weight:700;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pchip-val{grid-column:1/-1;font-size:16px;line-height:1.2;font-weight:800;color:var(--pc);font-variant-numeric:tabular-nums}
.pchip-sub{grid-column:1/-1;font-size:12px;line-height:1.25;color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.points-unavailable{flex:1;font-size:13px;color:var(--faint);padding:10px 4px}

/* ── home: wallet strip ──────────────────────────────────── */
/* [OWNER 2026-07-29] The balance had a home (three point chips in the app bar) but no destination.
   Measured the same day: 207 users, 14 holding points, 2 who ever spent any. The strip carries ONE
   total and ONE destination — repeating the three chips here would have said the same number twice
   and still not said what to do with it.
   [2026-07-31] Those app-bar chips and their rules are gone (game team: an unlabelled number in the
   corner does not say which point it is), so this strip is the only balance surface in the webview.
   Colours are combinations this file already passes the audit with: value = --boost on the tinted
   surface (11.3:1), label = --muted (stated AA on these surfaces), CTA = .btn-primary below
   (--on-accent on the gold gradient, 11.9:1 — the same fill .submit-btn/.cmt-send ship). No new token.
   The 44px floor on the CTA is load-bearing: spa-a11y audits HOME, so this strip is inside the
   touch-target baseline, not next to it. */
.wallet-strip{display:flex;align-items:center;gap:10px;margin:0 0 10px;padding:10px 12px;border-radius:14px;background:linear-gradient(120deg,rgba(255,194,51,.10),rgba(255,194,51,.08));border:1px solid rgba(255,194,51,.28)}
.wallet-strip-copy{display:flex;flex-direction:column;min-width:0;flex:1}
.wallet-strip-lb{font-size:12px;font-weight:700;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.wallet-strip-val{font-size:19px;line-height:1.2;font-weight:900;color:var(--boost);font-variant-numeric:tabular-nums}
.wallet-strip-cta{flex:none;max-width:58%;min-width:44px;min-height:44px;padding:0 14px;border-radius:12px;font-size:13px;font-weight:900;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* Shared Primary — filled gradient + contrast text. The same fill was already hand-copied into four
   places (.submit-btn, .cmt-send, .appbar-close, .fatal-close); new consumers take this one class
   instead of a fifth copy. Values are the existing --accent/--accent-2 (§6 C1 — no new token). */
.btn-primary{color:var(--on-accent);background:linear-gradient(90deg,var(--accent),var(--accent-2));border:1px solid rgba(255,206,92,.32);box-shadow:0 4px 14px rgba(0,0,0,.24)}
.btn-primary:disabled{color:var(--faint);background:var(--surface-3);border-color:var(--border);box-shadow:none}

/* ── home: announcements strip ───────────────────────────── */
/* [W9-UX3] The loading shape is a RESERVATION, not a promise: it holds exactly the box the filled
   strip will occupy so that nothing below it moves when the answer lands (a strip that arrives into
   unreserved space pushed every control down 182px and stole taps aimed at the quick links). It says
   nothing while it waits — no copy, no count, no shimmer — and it takes no pointer, because it is a
   held place and not a control. If the answer is empty or fails, the slot goes and the page closes
   UP, which cannot slide anything under a finger.
   The metrics below MUST stay equal to the filled strip's: .skel-title mirrors .home-notice
   .sec-title's line box (13px x the inherited 1.55) and its 2px/8px margins, .skel-row mirrors
   .notice-row (44px, 1px border, 6px gap), .notice-skel mirrors .home-notice's 10px tail. spa-ux3
   measures that equality — it does not take this comment's word for it. */
.home-notice{margin-bottom:10px}
.notice-skel{margin-bottom:10px;pointer-events:none}
.skel-title{font-size:13px;line-height:1.55;height:1.55em;margin:2px 2px 8px}
.skel-row{height:44px;border-radius:12px;background:var(--surface);border:1px solid var(--border-soft);margin-bottom:6px}
.home-notice .sec-title{margin-top:2px}
.notice-row{display:flex;align-items:center;gap:9px;width:100%;min-height:44px;padding:9px 12px;text-align:left;background:var(--surface);border:1px solid var(--border-soft);border-radius:13px;margin-bottom:6px;transition:border-color .12s}
.notice-row:active{border-color:var(--accent)}
.notice-ic{flex:none;font-size:13px}
.notice-title{flex:1;min-width:0;font-size:13px;font-weight:700;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.notice-time{flex:none;font-size:12px;color:var(--faint);font-variant-numeric:tabular-nums}

/* ── home: today's activity ──────────────────────────────── */
.card.daily{background:linear-gradient(135deg,rgba(61,220,132,.07),rgba(255,194,51,.06));border-color:rgba(61,220,132,.22)}
.daily-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}
.daily-h{font-size:14px;font-weight:800}
.daily-note{font-size:13px;color:var(--muted);line-height:1.6}

/* ── home: board rows ────────────────────────────────────── */
.board-row{display:flex;align-items:center;gap:12px;padding:14px;min-height:44px;width:100%;text-align:left;background:var(--surface);border:1px solid var(--border-soft);border-radius:14px;margin-bottom:8px;transition:border-color .12s}
.board-row:active{border-color:var(--accent)}
.board-ic{width:38px;height:38px;border-radius:11px;flex:none;font-size:18px;display:flex;align-items:center;justify-content:center;background:var(--surface-2);border:1px solid var(--border)}
.board-name{flex:1;min-width:0;font-size:15px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.board-arrow{color:var(--faint);font-size:16px;flex:none}

/* ── OWNER 2026-07-24: compact six-row home feed ────────── */
.home-feed{margin:0 0 18px}
.feed-section{margin-bottom:8px}
.feed-section .sec-title{margin:10px 2px 6px}
/* ★[v2A] The radius ladder now matches the mockup: rows 12–13, cards 14, hero 18. Before this, a home
   feed row was 12 and a list .post-card was 14 — the same thing (one row of a list) curved differently
   depending on which screen the reader was on. */
.feed-row{display:flex;align-items:center;gap:10px;width:100%;min-height:52px;padding:7px 10px;text-align:left;background:var(--surface);border:1px solid var(--border-soft);border-radius:13px;margin-bottom:6px;transition:border-color .12s}
.feed-row:active{border-color:var(--accent)}
.feed-thumb{width:36px;height:36px;flex:none;display:flex;align-items:center;justify-content:center;border-radius:10px;font-size:16px;background:var(--surface-2);border:1px solid var(--border)}
.feed-copy{display:flex;flex-direction:column;min-width:0;flex:1}
.feed-title{font-size:13.5px;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.feed-meta{display:flex;align-items:center;gap:5px;min-width:0;font-size:12px;color:var(--faint);white-space:nowrap;overflow:hidden}
.feed-meta .who{max-width:11ch;overflow:hidden;text-overflow:ellipsis}
.feed-row.official-post .feed-thumb{border-color:rgba(255,194,51,.42);background:rgba(255,194,51,.09)}
.home-screen .card.daily{margin-top:8px}

/* ── [W15-EARN] home: "how to earn points" guide ─────────── */
/* One collapsible strip above the wallet. Its whole cost when collapsed is a single 44px row, which
   is what lets it sit above the balance without pushing the feed down on every entry.
   Colour: the head reuses --boost (the wallet's own accent, so "points" reads as one subject across
   the two strips) and the rows reuse the .feed-row/.notice-row surface + --border-soft treatment, so
   this introduces no colour literal that is not already in the file (UIUX ③-h pins the exact set).
   No :hover rules here — the touch fence lives further down and a stuck hover state on a strip the
   user taps through would paint "selected" on it permanently (UIUX ③-a). */
/* ★[AC-18 · v2A] This strip is WHAT HOME ACTUALLY DRAWS WHILE THE VALVE IS SHUT — the mission card
   stands only where the server opened it (home.mjs: `mission ? null : earnGuide(ctx)`), and W18-ONBOARD
   shipped valve-off. Leaving it out of the reskin would have retoned every home except the one live
   users are looking at.
   Three moves, all alphas of tokens already here: ① a gold-tinted container border — the head's gold
   gradient did not meet the container, so it read as a loose band; ② a hairline under the head — with
   no boundary the list bled into the head in both collapsed and expanded states; ③ a surface on the
   row so it reads as something to press — the row had no background at all, which was the one axis on
   which it diverged from .onb-row, the other "list of routes" on this same screen. */
.home-earn{margin:0 0 10px;border-radius:14px;background:var(--surface);border:1px solid rgba(255,194,51,.22);overflow:hidden}
.earn-head{display:flex;align-items:center;gap:8px;width:100%;min-height:44px;padding:9px 12px;text-align:left;background:linear-gradient(120deg,rgba(255,194,51,.11),rgba(255,194,51,.06));border-bottom:1px solid rgba(255,194,51,.16)}
.earn-head-lb{flex:1;min-width:0;font-size:13px;font-weight:800;color:var(--boost);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* The chevron reads the SAME attribute the panel does — one home for "is it open", so the arrow can
   never point the wrong way at a panel that is open (a second flag here would eventually disagree).
   ★ --faint → --muted: this glyph is the ONLY sign that the strip collapses at all, and it was drawn
     in the faintest ink the palette has. It is an affordance, not an ornament. */
.earn-chev{flex:none;font-size:14px;line-height:1;color:var(--muted);transition:transform .16s}
.earn-head[aria-expanded="true"] .earn-chev{transform:rotate(180deg)}
.earn-list{padding:8px}
.earn-row{display:flex;align-items:center;gap:9px;width:100%;min-height:44px;padding:8px 10px;text-align:left;border-radius:12px;background:rgba(5,7,13,.30);border:1px solid var(--border-soft);transition:background-color .12s}
.earn-row + .earn-row{margin-top:6px}
.earn-row:active{background:var(--surface-2)}
/* [W18-ONBOARD] The tick slot and the finished-row rule are GONE with the nodes that consumed them.
   Both existed to draw a completion state this strip inferred from one page of the viewer's own
   posts; the server states that fact outright now (see the mission card below) and the strip is a
   list of routes again. A rule with no node left to match is a rule that quietly goes stale. */
.earn-copy{display:flex;flex-direction:column;min-width:0;flex:1}
.earn-name{font-size:13.5px;font-weight:700;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.earn-sub{font-size:12px;line-height:1.4;color:var(--faint)}
.earn-arrow{flex:none;font-size:15px;line-height:1;color:var(--faint)}

/* ── [W18-ONBOARD] home: the new-member mission card ─────── */
/* An EVENT board, not a list. The ribbon, the filled step pips and the reward band underneath are
   what make three ordinary rows read as something that is running right now — the order's words were
   "a label on its own gives nobody a reason to act".
   No new colour and no new token (UIUX 3-h pins the file's hex set exactly): --boost carries "points"
   here exactly as it does on the wallet strip and the earn head, so the subject reads as one thing
   across the screen; --playboost carries "finished", the same green the activity card already uses;
   the reward CTA is the shared .btn-primary fill. Every tint below is an alpha of one of those.
   The 44px floors are load-bearing — spa-a11y audits HOME, so every control in this card is inside
   the touch-target baseline rather than next to it.
   No :hover rules: the fence lives further down and a stuck hover on a card the user taps through
   would paint "selected" on it permanently (UIUX 3-a). Press feedback is the shared --press. */
/* ★[AC-18 · v2A] This card was authored on the BURGUNDY palette and carried three of its literals in
   with the merge — the two far stops of this gradient, the .onb-plate.go border, and .onb-reward.ready.
   Git merged them silently (neither side edited the same line); m21e T-THEME is what stood them up in
   red, which is the whole reason that assertion pins "no burgundy left" rather than "gold present".
   Only VALUES move here: gold (--accent) → amber (--accent-2) → deep navy (--surface-2), so the
   mockup hero's "warm at the top, deep at the bottom" axis survives and only the hue changes.
   New hexes: zero — every stop is an alpha of a value already in the file (UIUX ③-h pins that set). */
.home-onboard{
  position:relative;overflow:hidden;
  margin:0 0 12px;padding:14px 13px 13px;border-radius:18px;
  background:linear-gradient(150deg,rgba(255,194,51,.13),rgba(255,143,79,.07) 55%,rgba(26,36,64,.55));
  border:1px solid rgba(255,194,51,.35);
  box-shadow:0 6px 22px rgba(0,0,0,.30);
}
/* The mockup's corner glow. Pure decoration, so it must not be readable: an empty ::before with
   pointer-events:none is in neither the accessibility tree nor the hit test. Children are lifted to
   position:relative so they paint above it. */
.home-onboard::before{
  content:'';position:absolute;top:-52px;right:-38px;width:170px;height:170px;border-radius:50%;
  background:radial-gradient(circle,rgba(255,194,51,.20),transparent 70%);pointer-events:none;
}
.home-onboard > *{position:relative}
.onb-head{display:flex;align-items:center;gap:7px;margin-bottom:6px}
/* Ink on an accent fill has ONE home, --on-accent (see the token layer). The previous --bg is also
   dark, so the contrast passed — but it passed by coincidence: --bg is "the page ground", not "ink on
   a fill", and when the ground next moves this badge would have followed the wrong one. */
.onb-badge{flex:none;font-size:12px;font-weight:900;letter-spacing:.08em;line-height:1.45;padding:2px 8px;border-radius:999px;color:var(--on-accent);background:var(--boost)}
.onb-title{flex:1;min-width:0;font-size:15px;font-weight:900;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.onb-count{flex:none;font-size:12px;font-weight:800;color:var(--boost);font-variant-numeric:tabular-nums}
.onb-lead{font-size:12px;line-height:1.5;color:var(--muted);margin-bottom:9px}
/* Filled pips — decoration for the eye only. The head states the same count in words, which is why
   the markup hides this row from the reader instead of announcing the fact twice. */
.onb-steps{display:flex;gap:4px;margin-bottom:10px}
.onb-step{flex:1;height:4px;border-radius:999px;background:rgba(255,194,51,.16)}
.onb-step.on{background:var(--boost)}
.onb-list{list-style:none;display:flex;flex-direction:column;gap:6px}
.onb-row{display:flex;align-items:center;gap:8px;min-height:52px;padding:7px 8px 7px 11px;border-radius:12px;background:rgba(5,7,13,.34);border:1px solid rgba(255,194,51,.16)}
.onb-row.done{background:rgba(61,220,132,.07);border-color:rgba(61,220,132,.26)}
.onb-name{flex:1;min-width:0;font-size:13.5px;font-weight:700;line-height:1.4;color:var(--text)}
/* The figure is the reason to act, so it is sized and coloured like one — the same gold and the same
   tabular digits as the wallet total, so what a row pays and where it lands read as one kind of
   number. A row whose amount the answer did not carry renders NO node here at all. */
.onb-amt{flex:none;font-size:14px;font-weight:900;color:var(--boost);font-variant-numeric:tabular-nums;white-space:nowrap}
/* The plate. Both states are controls carrying WORDS; what separates them is which one answers a tap.
   The open one is an outlined accent button, deliberately one tier below the reward CTA so the card
   has a single loudest control and it is the one that pays. The finished one is `disabled` — out of
   the tab order, and out of the audit that would otherwise measure it — and its opacity is restored
   because the global disabled fade means "unavailable" while this plate means "achieved". */
.onb-plate{flex:none;min-width:64px;min-height:44px;padding:0 12px;border-radius:11px;font-size:12.5px;font-weight:800;white-space:nowrap}
.onb-plate.go{color:var(--accent-text);background:var(--surface-2);border:1px solid rgba(255,194,51,.42);transition:background-color .16s ease,transform .11s ease}
.onb-plate.go:not(:disabled):active{background:var(--surface-3);transform:var(--press)}
.onb-plate.done{color:var(--playboost);background:rgba(61,220,132,.12);border:1px solid rgba(61,220,132,.36);opacity:1}
/* The completion reward: large, and underneath the rows, because it is the answer to "and what do I
   get for doing all three". It stays fully visible while locked — a reward nobody can see is not an
   incentive — and the dashed edge plus the note under the button carry the "not yet" instead. */
.onb-reward{margin-top:11px;padding:11px 12px;border-radius:13px;background:rgba(5,7,13,.42);border:1px dashed rgba(255,194,51,.34)}
.onb-reward.ready{background:linear-gradient(120deg,rgba(255,194,51,.18),rgba(255,143,79,.10));border-style:solid;border-color:rgba(255,194,51,.62)}
.onb-reward-copy{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-bottom:9px}
.onb-reward-lb{font-size:12px;font-weight:800;color:var(--muted);white-space:nowrap}
.onb-reward-val{font-size:26px;line-height:1.15;font-weight:900;color:var(--boost);font-variant-numeric:tabular-nums}
.onb-claim{width:100%;min-width:44px;min-height:48px;padding:0 16px;border-radius:13px;font-size:15px;font-weight:900}
.onb-locked{margin-top:7px;font-size:12px;line-height:1.45;color:var(--faint);text-align:center}

/* ── [W18-ONBOARD] home: attendance stamps ───────────────── */
/* This card takes the slot the "not ready yet" note holds, and it borrows that slot's green so the
   two never look like two different features swapping places.
   The cell COUNT comes from the markup, which takes it from the server's card_size — the grid is
   auto-fit rather than a fixed column count, so a five-cell or a ten-cell card lays itself out
   without a second copy of that number living in this file. */
/* ★[v2A] The second stop was burgundy — the green tint now settles into deep navy instead. The green
   this card BORROWS from the "not ready yet" card is the first stop and is untouched, so the property
   that made the borrow worth doing (the two never look like different features swapping places)
   survives the palette move. */
.card.attend{background:linear-gradient(135deg,rgba(61,220,132,.09),rgba(26,36,64,.55));border-color:rgba(61,220,132,.24)}
.home-screen .card.attend{margin-top:8px}
.att-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}
.att-h{font-size:14px;font-weight:800}
.att-board{display:flex;align-items:center;gap:9px;margin-bottom:10px}
.att-cells{flex:1;min-width:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(30px,1fr));gap:5px}
.att-cell{display:flex;align-items:center;justify-content:center;aspect-ratio:1;border-radius:10px;font-size:12px;font-weight:800;color:var(--faint);background:var(--surface-2);border:1px solid var(--border-soft)}
.att-cell.on{color:var(--playboost);background:rgba(61,220,132,.15);border-color:rgba(61,220,132,.42)}
.att-count{flex:none;font-size:12px;font-weight:800;color:var(--muted);font-variant-numeric:tabular-nums}
.att-note{font-size:12px;line-height:1.5;color:var(--muted);margin-bottom:10px}
.att-note.earned{color:var(--playboost);font-weight:700}
.att-cta{width:100%;min-width:44px;min-height:48px;padding:0 16px;border-radius:13px;font-size:14.5px;font-weight:900}
/* Today's stamp is taken: the control says so and stops being a call to action. Opacity restored for
   the same reason as the mission plate — "achieved", not "unavailable". */
.att-cta.done{color:var(--playboost);background:rgba(61,220,132,.12);border:1px solid rgba(61,220,132,.36);opacity:1}
/* [OWNER 2026-08-15 · AC-18] When the daily reward is paid. It sits UNDER the control it qualifies
   and it is never absent — the settlement time has to be readable before the tap, not only after it.
   Quiet in its idle register (a standing rule), promoted to --muted once it is stating today's own
   pending figure, so the eye lands on it exactly when it has become a specific promise. */
/* ★[AC-18 · v2A] 11.5px → 12px. §2.6's caption floor is 12 and this line was under it, along with
   .att-cta-sub and .att-warp-more below. Nothing was wrong with the predictor: spa-a11y audits HOME,
   but the audit's home has never had an onboarding answer routed, so the event board and this stamp
   card were simply outside what it walks — the same shape of gap that hid the shop's four violations
   (that time the screen was missing from the list; this time the STATE was). The fixture now routes
   the cards and asserts they are on screen before auditing, so these sizes are measured from here on. */
.att-settle{margin-top:8px;font-size:12px;line-height:1.5;color:var(--faint);text-align:center}
.att-settle.pending{color:var(--muted);font-weight:700}
/* [OWNER 2026-08-15 · AC-19] One stamp now accrues TWO kinds of point, so the CTA carries two lines:
   the verb on top, the named figures under it. This is a MODIFIER — the base rule above still owns the
   48px floor, the radius and the weight — and the markup only asks for it when a warp figure actually
   exists, so a card with warpboost unseeded keeps its one-line button unchanged.
   The kind names sit at the 11.5px caption floor rather than being abbreviated: they are the only
   thing that stops 1,000P and 500P from reading as one balance broken in two. */
.att-cta.two{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;padding-top:7px;padding-bottom:7px;line-height:1.25}
.att-cta-main{font-size:14.5px;font-weight:900}
.att-cta-sub{font-size:12px;font-weight:800;letter-spacing:-.1px;opacity:.94}
/* The warpboost line: one sentence saying what it is, and the door to where its terms actually live
   (the shop). It wears the point kind's OWN token (--warpboost) so the sentence is visibly tied to the
   figure the button just named, and it is a full-width row at the 44px touch floor because it is a
   route, not a footnote. The sentence WRAPS rather than truncating — an explanation cut off by an
   ellipsis explains nothing, and this row exists only to explain. */
.att-warp{display:flex;align-items:center;gap:8px;width:100%;min-width:44px;min-height:44px;margin-top:9px;padding:8px 10px;border-radius:12px;text-align:left;background:var(--surface-2);border:1px solid rgba(195,166,255,.32);transition:background-color .16s ease,transform .11s ease}
.att-warp:active{background:var(--surface-3);transform:var(--press)}
.att-warp-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.att-warp-name{font-size:12px;line-height:1.45;font-weight:800;color:var(--warpboost)}
.att-warp-more{font-size:12px;line-height:1.4;font-weight:700;color:var(--faint)}
.att-warp-arrow{flex:none;font-size:15px;line-height:1;color:var(--faint)}

/* ── [OWNER 2026-08-15] the checklist's COMPACT bar, and the vertical diet behind it ───── */
/* The order was that the DEFAULT STATE is a compact one-line bar — not a collapsible card
   with the old body still inside it. So the bar IS the card at rest: ~48px, one row, carrying
   everything the reader needs to decide whether to open it. Expanded it keeps every element it
   had, at reduced padding. No token VALUE moves here (the palette is pinned); only geometry. */
.onb-bar{display:flex;align-items:center;gap:8px;width:100%;min-height:48px;padding:6px 10px 6px 2px;text-align:left;background:none;border:0;color:var(--text)}
.onb-bar .onb-head{flex:1;min-width:0;margin-bottom:0;gap:6px}
.onb-bar .onb-title{font-size:14px}
.onb-bar .onb-badge{font-size:12px;padding:2px 6px}
.onb-bar .onb-count{font-size:12px}
.onb-bar-reward{flex:none;font-size:12px;font-weight:900;color:var(--boost);font-variant-numeric:tabular-nums;white-space:nowrap}
.onb-bar-chev{flex:none;font-size:14px;line-height:1;color:var(--muted);transition:transform .16s}
.onb-bar[aria-expanded="true"] .onb-bar-chev{transform:rotate(180deg)}
.onb-body{padding-top:2px}
/* Slimmer rows: 52 → 46px, and the lead/pips/band lose roughly a third of their spacing each. The
   44px tap-target floor is held by .onb-plate, which is unchanged — the ROW got shorter, not the
   control inside it. Caption sizes are untouched (the 12px a11y floor is a hard line, not a budget). */
.home-onboard .onb-lead{margin-bottom:6px}
.home-onboard .onb-steps{margin-bottom:7px}
.home-onboard .onb-row{min-height:46px;padding:5px 8px 5px 11px}
.home-onboard .onb-reward{margin-top:8px;padding:9px 12px}
.home-onboard .onb-reward-copy{margin-bottom:7px}
.home-onboard .onb-reward-val{font-size:22px}
.home-onboard .onb-claim{min-height:46px}
/* The attendance card, same yardstick: cells keep their size (they ARE the completion rule), the
   space between them shrinks, and the two prose lines tighten. */
.card.attend .att-head{margin-bottom:7px}
.card.attend .att-board{margin-bottom:7px}
.card.attend .att-note{margin-bottom:7px}
.card.attend .att-settle{margin-top:6px}
.card.attend .att-warp{margin-top:7px;min-height:44px;padding:6px 10px}

/* ── [W18-DASH · sian v2A] TODAY REWARDS ───── */
/* The mockup's card, rebuilt on the token layer. Its gradient is three raw hexes in the mockup
   (a deep violet, a mid blue, a navy); expressing it as alpha over --frame-bg reproduces the same
   wash without adding three literals to the palette set that samez-uiux ③-h pins exactly. rgba()
   is not counted by that pin, and this file has composed alpha over tokens since it was written. */
.today{position:relative;overflow:hidden;margin:0 0 10px;padding:16px 15px 13px;border-radius:18px;
  background:linear-gradient(135deg,rgba(177,140,255,.24) 0%,rgba(91,140,255,.18) 60%,rgba(5,7,13,.34) 100%),var(--frame-bg);
  border:1px solid rgba(255,194,51,.35)}
.today-glow{position:absolute;top:-50px;right:-36px;width:170px;height:170px;border-radius:50%;background:radial-gradient(circle,rgba(255,194,51,.22),transparent 70%)}
.today > *{position:relative}
.today-row{display:flex;align-items:center;gap:14px}
.today-copy{flex:1;min-width:0}
.today-eyebrow{font-size:12px;font-weight:800;letter-spacing:.10em;color:var(--violet-ink)}
/* The mockup sets 29px here. Kept verbatim — it is the one number on this screen that is meant to be
   read from across a room, and nothing about it fights the a11y floor (floors are minimums). */
.today-total{margin-top:2px;line-height:1.15}
.today-total-n{font-size:29px;font-weight:900;letter-spacing:-.01em;font-variant-numeric:tabular-nums;color:var(--text)}
.today-total-lb{font-size:13px;font-weight:700;color:var(--muted);margin-left:5px}
/* [W18-TRAINC · OWNER 2026-08-15] "received / receivable today" — the ring's percentage in words,
   beside the headline it explains. 13px, not the 12px floor: this is the second-most-read figure on
   the card and its whole job is to make the ring legible, so it sits one step above the minimum
   rather than on it. Tabular numerals so the two figures stay column-aligned as they grow. */
.today-frac{margin-top:2px;font-size:13px;font-weight:800;color:var(--muted);
  font-variant-numeric:tabular-nums;letter-spacing:-.01em}
/* Mockup: 10.5px. RAISED to 12px — the a11y floor is a hard line, and this pill carries the one
   sentence that says the money has not arrived yet. Logged as an a11y correction. */
.today-cd{display:inline-flex;align-items:center;gap:5px;margin-top:8px;padding:3px 10px;border-radius:999px;
  font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;
  color:var(--boost);background:rgba(255,194,51,.10);border:1px solid rgba(255,194,51,.40)}
/* The nudge. A caption-sized route, at the 44px target floor — it is a real control, not a label. */
.today-hook{display:block;margin-top:7px;min-height:44px;padding:0;text-align:left;font-size:12px;font-weight:800;color:var(--violet-ink)}
.today-hook:active{opacity:.75}
.today-ring{position:relative;width:76px;height:76px;flex:none}
.today-ring svg{transform:rotate(-90deg)}
.ring-track{stroke:rgba(30,41,70,.9)}
.ring-fill{transition:stroke-dashoffset .6s ease}
.ring-a{stop-color:var(--boost)}
.ring-b{stop-color:var(--playboost)}
@media (prefers-reduced-motion:reduce){.ring-fill{transition:none}}
.today-ring-pct{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:800;font-variant-numeric:tabular-nums;color:var(--text)}
/* The divider the mockup draws in its lead violet; same stop this file uses everywhere else. */
.today-wallet{display:flex;gap:0;margin-top:13px;padding-top:11px;border-top:1px solid rgba(177,140,255,.25)}
.today-w{flex:1;min-width:0;display:flex;align-items:center;gap:6px}
.today-w-key{width:18px;height:18px;flex:none;border-radius:6px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;line-height:1}
.today-w.b .today-w-key{color:var(--boost);background:rgba(255,194,51,.12);border:1px solid rgba(255,194,51,.42)}
.today-w.p .today-w-key{color:var(--playboost);background:rgba(61,220,132,.12);border:1px solid rgba(61,220,132,.42)}
.today-w.w .today-w-key{color:var(--warpboost);background:rgba(195,166,255,.12);border:1px solid rgba(195,166,255,.42)}
.today-w-copy{display:flex;flex-direction:column;min-width:0}
/* Mockup: 8.5px. RAISED to 12px — same floor, same reason. These three labels are what keep the
   figures beside them from reading as one balance split in three. */
.today-w-name{font-size:12px;font-weight:700;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.today-w-val{font-size:13px;font-weight:800;line-height:1.2;font-variant-numeric:tabular-nums}
.today-w.b .today-w-val{color:var(--boost)}
.today-w.p .today-w-val{color:var(--playboost)}
.today-w.w .today-w-val{color:var(--warpboost)}
/* Mockup: 10.5px green line. RAISED to 12px, and it is a real button — the mockup's line names the
   shop, and a sentence that names a destination with no way to reach it is a dead end. */
.today-sum{display:block;width:100%;min-height:44px;margin-top:9px;padding:4px 0;text-align:left;font-size:12px;font-weight:700;color:var(--playboost)}

/* ── [W18-RANK] weekly ranking hero (home) ───── */
/* A single button, violet-led so it does not read as a third gold card stacked on the checklist and
   the wallet. The prize figure is the only large number on it: that is the thing worth crossing the
   screen for, and everything else on the card exists to say whether the reader is near it. */
.rank-hero{display:block;width:100%;text-align:left;margin:0 0 10px;padding:11px 13px;border-radius:16px;background:linear-gradient(135deg,rgba(177,140,255,.26),rgba(91,140,255,.14) 62%,rgba(5,7,13,.22));border:1px solid var(--violet-line);color:var(--text);box-shadow:0 6px 22px rgba(177,140,255,.12)}
.rank-head{display:flex;align-items:center;gap:7px;margin-bottom:5px}
.rank-badge{flex:none;font-size:12px;font-weight:900;letter-spacing:.10em;color:var(--violet-ink)}
/* The honesty badge. It is a caption, so it sits AT the 12px floor and not below it — the whole
   point of this element is that it gets read. */
.rank-pending{flex:none;font-size:12px;font-weight:800;line-height:1.35;padding:1px 7px;border-radius:999px;color:var(--boost);background:rgba(255,194,51,.12);border:1px solid rgba(255,194,51,.42)}
.rank-counting{flex:none;font-size:12px;font-weight:800;line-height:1.35;padding:1px 7px;border-radius:999px;color:var(--playboost);background:rgba(61,220,132,.10);border:1px solid rgba(61,220,132,.40)}
.rank-row{display:flex;align-items:center;gap:10px}
.rank-body{flex:1;min-width:0;display:block}
.rank-title{display:block;font-size:14px;font-weight:900;color:var(--text)}
.rank-meta{display:flex;align-items:baseline;flex-wrap:wrap;gap:5px;margin-top:2px;font-size:12px;color:var(--muted)}
.rank-my{color:var(--text);font-weight:700}
.rank-my-cnt{margin-left:5px;color:var(--muted);font-weight:600}
.rank-ends,.rank-cd{color:var(--muted);font-variant-numeric:tabular-nums}
.rank-cd{font-weight:800;color:var(--violet-ink)}
.rank-empty{flex:1;min-width:0;display:block}
.rank-empty-t{display:block;font-size:13.5px;font-weight:800;color:var(--text)}
.rank-empty-b{display:block;margin-top:2px;font-size:12px;line-height:1.45;color:var(--muted)}
.rank-prize{flex:none;text-align:right;display:block}
.rank-prize-lb{display:block;font-size:12px;font-weight:700;color:var(--muted)}
.rank-prize-val{display:block;font-size:20px;line-height:1.15;font-weight:900;color:var(--boost);font-variant-numeric:tabular-nums}
/* The mockup's progress rail under the hero — violet to blue, its own two stops. */
.rank-bar{display:block;height:5px;border-radius:99px;background:var(--surface-3);margin-top:10px;overflow:hidden}
.rank-bar-fill{display:block;height:100%;border-radius:99px;background:linear-gradient(90deg,var(--violet),var(--info-2))}

/* ── [W18-RANK] the two period blocks (events tab) ───── */
.rank-section{display:flex;flex-direction:column;gap:10px;margin:0 0 12px}
.rankb{padding:13px;border-radius:16px;background:linear-gradient(160deg,rgba(177,140,255,.10),transparent 58%),var(--surface);border:1px solid var(--violet-line)}
.rankb.monthly{background:linear-gradient(160deg,rgba(255,194,51,.10),transparent 58%),var(--surface);border-color:rgba(255,194,51,.38)}
.rankb-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
.rankb-badge{font-size:12px;font-weight:900;letter-spacing:.10em;color:var(--violet-ink)}
.rankb.monthly .rankb-badge{color:var(--accent-text)}
.rankb-title{font-size:15.5px;font-weight:900;margin-top:4px;color:var(--text)}
.rankb-lead{font-size:12px;line-height:1.45;color:var(--muted);margin-top:2px}
.rankb-deadline{display:flex;align-items:baseline;gap:6px;margin-top:8px;font-size:12px;color:var(--muted)}
.rankb-cd{font-size:17px;font-weight:900;color:var(--accent-text);font-variant-numeric:tabular-nums}
/* The prize ladder. auto-fit rather than a fixed five, because the number of cells is the number of
   prizes the server named — a policy paying three or seven renders correctly with no CSS change. */
.rankb-ladder{display:grid;grid-template-columns:repeat(auto-fit,minmax(52px,1fr));gap:6px;margin-top:10px}
.rankb-cell{text-align:center;padding:6px 2px;border-radius:10px;background:rgba(5,7,13,.34);border:1px solid var(--border-soft)}
.rankb-cell.top{background:rgba(255,194,51,.10);border-color:rgba(255,194,51,.40)}
.rankb-cell-rank{font-size:12px;font-weight:800;color:var(--muted)}
.rankb-cell.top .rankb-cell-rank{color:var(--accent-text)}
.rankb-cell-amt{font-size:12.5px;font-weight:900;color:var(--boost);font-variant-numeric:tabular-nums;margin-top:1px}
.rankb-list{list-style:none;margin:10px 0 0;padding:0;display:flex;flex-direction:column;gap:5px}
.rankb-row{display:flex;align-items:center;gap:8px;padding:6px 9px;border-radius:11px;background:rgba(5,7,13,.30);border:1px solid var(--border-soft)}
.rankb-row.first{background:rgba(255,194,51,.09);border-color:rgba(255,194,51,.34)}
.rankb-n{flex:none;min-width:26px;font-size:12px;font-weight:900;color:var(--accent-text);font-variant-numeric:tabular-nums}
.rankb-name{flex:1;min-width:0;font-size:13px;font-weight:700;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rankb-cnt{flex:none;font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
.rankb-amt{flex:none;font-size:12.5px;font-weight:900;color:var(--boost);font-variant-numeric:tabular-nums}
.rankb-mine{margin-top:9px;padding:8px 10px;border-radius:11px;background:rgba(5,7,13,.42);border:1px solid rgba(177,140,255,.30);font-size:12.5px}
.rank-note{margin-top:7px;font-size:12px;line-height:1.45;color:var(--faint)}

/* ── [W18-RANK] the point-benefit directory (events tab) ───── */
.events-screen{display:block}
.benefit-sec{margin:0 0 12px}
.benefit-head{margin:0 2px 8px}
.benefit-title{font-size:13px;font-weight:900;letter-spacing:.06em;color:var(--muted)}
.benefit-lead{margin-top:2px;font-size:12px;line-height:1.45;color:var(--faint)}
.benefit-list{display:flex;flex-direction:column;gap:6px}
.benefit-row{display:flex;align-items:center;gap:8px;width:100%;min-height:48px;padding:8px 10px;text-align:left;border-radius:12px;background:var(--surface);border:1px solid var(--border-soft);color:var(--text)}
.benefit-row:active{background:var(--surface-2)}
/* Finished rows dim, but the WORD on the plate is what carries the state — the tint only agrees. */
.benefit-row.done{opacity:.72}
.benefit-name{flex:1;min-width:0;font-size:13.5px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.benefit-amt{flex:none;font-size:13px;font-weight:900;color:var(--boost);font-variant-numeric:tabular-nums;white-space:nowrap}
.benefit-badge{flex:none;font-size:12px;font-weight:800;line-height:1.35;padding:1px 7px;border-radius:999px;color:var(--accent-text);background:rgba(255,194,51,.10);border:1px solid rgba(255,194,51,.34);white-space:nowrap}
.benefit-badge.done{color:var(--playboost);background:rgba(61,220,132,.10);border-color:rgba(61,220,132,.36)}
.benefit-arrow{flex:none;font-size:15px;line-height:1;color:var(--faint)}

/* ── [W18-ONBOARD] "more ways to earn" follow-up panel ───── */
/* Same overlay posture as the claim modal, for the same reasons: it mounts inside #app so it covers
   the frame, it owns NO history entry (the meaning of the back gesture on home is a pinned
   navigation invariant), and it is dismissed by its own control, the scrim or Escape.
   Its own class names, though — the claim modal's are bound by specs that must keep matching exactly
   one element, and a second .claim-card on the same screen would break their strict-mode locators. */
/* The scrim is the SAME value as its sibling .claim-overlay. The comment above says "same overlay
   posture as the claim modal"; a posture stated in prose and contradicted by the value means the two
   modals dim the frame by different amounts — the old palette's near-black was exactly that split. */
.more-overlay{position:absolute;inset:0;z-index:50;background:rgba(3,4,8,.82);display:flex;align-items:center;justify-content:center;padding:20px;animation:vfade .16s ease}
@media (prefers-reduced-motion:reduce){.more-overlay{animation:none}}
.more-card{width:100%;max-width:340px;padding:20px 16px 16px;border-radius:18px;background:var(--surface-2);border:1px solid rgba(255,194,51,.34);box-shadow:0 18px 48px rgba(0,0,0,.55)}
.more-title{margin:0 0 6px;font-size:17px;line-height:1.35;font-weight:900;color:var(--boost);text-align:center}
.more-body{margin:0 0 12px;font-size:12.5px;line-height:1.55;color:var(--muted);text-align:center}
.more-list{padding:0;margin-bottom:14px}
.more-card .earn-row{background:var(--surface);border:1px solid var(--border-soft);margin-bottom:6px}
.more-close{width:100%;min-width:44px;min-height:44px;padding:0 16px;border-radius:12px;font-size:14px;font-weight:800}

/* ── list: post cards ────────────────────────────────────── */
/* [AC-18 · v2A] The row became a card with a stated hierarchy: title (heavier, roomier), then one
   quiet meta line, then the two counts. Only spacing/type/colour move here — the DOM the list emits
   is unchanged except for the stat row's inner spans (screens/list.mjs). */
.post-card{display:block;width:100%;text-align:left;background:var(--surface);border:1px solid var(--border-soft);border-radius:14px;padding:14px 14px 12px;margin-bottom:9px;min-height:44px;transition:border-color .12s}
.post-card:active{border-color:var(--accent)}
.official-post{border-color:rgba(255,194,51,.34);background:linear-gradient(105deg,rgba(255,194,51,.075),var(--surface) 42%);box-shadow:inset 3px 0 0 rgba(255,194,51,.62)}
.post-title{font-size:15px;font-weight:800;line-height:1.42;letter-spacing:-.01em;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word}
.post-meta{display:flex;align-items:center;gap:7px;margin-top:9px;font-size:12px;color:var(--faint);flex-wrap:wrap}
.post-meta .who{display:inline-flex;align-items:center;gap:6px;color:var(--muted)}
/* The avatar mark is a PSEUDO-element on the author name, so it exists only where a name does. An
   official post renders `.gm-badge` instead of `.who` and therefore never grows one — the shield is
   that author's mark and a second dot beside it would read as a second author. */
.post-meta .who::before{content:"";flex:none;width:16px;height:16px;border-radius:999px;background:linear-gradient(135deg,rgba(255,206,92,.34),rgba(127,163,255,.28));border:1px solid var(--border)}
.post-meta .sep{color:var(--faint)}
/* Counts sit on their own line: on a 390px screen the author, the time and two counts do not fit on
   one, and wrapping them left an orphan count under a half-empty row. */
.post-stats{display:flex;align-items:center;gap:14px;margin-top:8px;font-size:12px;color:var(--faint)}
.post-stat{display:inline-flex;align-items:baseline;gap:5px}
.post-stat-lb{color:var(--faint)}
.post-stat-n{font-weight:800;color:var(--muted);font-variant-numeric:tabular-nums}
/* The reaction count is the accent one because it is the number the weekly ranking is computed from
   — the comment count is context, this one is the score. */
.post-stat-reactions .post-stat-lb,.post-stat-reactions .post-stat-n{color:var(--accent-text)}
.more-btn{width:100%;font-size:14px;font-weight:700;color:var(--accent-text);background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:12px;min-height:44px;margin-top:4px}

/* ── detail ──────────────────────────────────────────────── */
.detail-official{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:0 0 12px;padding:9px 11px;border:1px solid rgba(255,194,51,.32);border-radius:12px;background:linear-gradient(105deg,rgba(255,194,51,.10),rgba(255,194,51,.025));box-shadow:inset 3px 0 0 rgba(255,194,51,.65)}
.detail-official-time{font-size:12px;color:var(--faint);font-variant-numeric:tabular-nums}
/* [AC-18 · v2A] The ordinary author's line (screens/detail.mjs byline). It is deliberately NOT a
   quieter copy of `.detail-official`: that treatment — the tinted panel and the inset gold edge — is
   the operator mark, and an ordinary post that wore a faded version of it would be claiming a weaker
   form of the same thing. This is a plain rule-under-the-name, which is what a byline is. */
.detail-byline{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin:0 2px 12px;padding-bottom:11px;font-size:12px;color:var(--faint);border-bottom:1px solid var(--border-soft)}
.detail-byline .who{display:inline-flex;align-items:center;gap:6px;color:var(--muted);font-weight:700}
.detail-byline .who::before{content:"";flex:none;width:18px;height:18px;border-radius:999px;background:linear-gradient(135deg,rgba(255,206,92,.34),rgba(127,163,255,.28));border:1px solid var(--border)}
.detail-byline-time{font-variant-numeric:tabular-nums}
.detail-body{font-size:15px;line-height:1.8;color:var(--text);padding:4px 2px 2px;white-space:pre-line;word-break:break-word}
.detail-body.md{white-space:normal}
.md-p{min-height:.4em;margin:0 0 2px}
.md-p:empty{height:.7em}
.md-ul{margin:6px 0 6px 18px;padding:0}
.md-ul li{margin:2px 0;list-style:disc}
.md-quote{border-left:3px solid var(--accent);background:var(--surface-2);border-radius:0 8px 8px 0;padding:8px 12px;margin:8px 0;color:var(--muted)}
.code-copy{display:flex;align-items:center;gap:10px;background:var(--surface-2);border:1px dashed var(--accent-text);border-radius:10px;padding:10px 12px;margin:8px 0}
.code-copy-value{flex:1;font-weight:900;font-size:16px;letter-spacing:.06em;word-break:break-all;user-select:all;-webkit-user-select:all}
.code-copy-btn{flex:none;min-height:44px;min-width:64px;padding:0 14px;border-radius:10px;font-size:13px;font-weight:900;color:var(--on-accent);background:linear-gradient(90deg,var(--accent),var(--accent-2));border:1px solid rgba(255,206,92,.32)}
.code-copy-shim{position:fixed;top:-200px;left:0;opacity:0}
.md strong{font-weight:800;color:var(--text)}
.md em{font-style:italic}
.rec-wrap{display:flex;flex-direction:column;align-items:center;gap:8px;margin:22px 0 8px}
.rec-btn{display:inline-flex;align-items:center;gap:8px;font-size:15px;font-weight:800;color:var(--boost);background:rgba(255,194,51,.08);border:1.5px solid rgba(255,194,51,.5);border-radius:999px;padding:12px 26px;min-height:44px;transition:transform .1s,background .15s}
.rec-btn:active{transform:scale(.96)}
.rec-btn.done{color:var(--on-accent);background:var(--boost);border-color:var(--boost);pointer-events:none}
.rec-note{font-size:12px;color:var(--faint)}
/* [AC-18 · v2A] Comments became cards. A hairline-separated stack reads as one continuous block of
   14px text on a phone — you cannot see where one comment ends without reading it — and the per
   comment controls that land here later would have no box to sit in. Same nodes, same order, new
   frame. `border-bottom:none` on the last child stays even though there is no bottom border left to
   clear: it is what makes a future revert to the separated stack a one-line change. */
.cmt-list{display:flex;flex-direction:column;gap:8px;margin-top:6px}
.cmt{padding:11px 12px;background:var(--surface);border:1px solid var(--border-soft);border-radius:12px}
.cmt:last-child{border-bottom:none}
.cmt-head{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--muted);margin-bottom:5px}
.cmt-head .who{display:inline-flex;align-items:center;gap:6px;font-weight:700}
.cmt-head .who::before{content:"";flex:none;width:15px;height:15px;border-radius:999px;background:linear-gradient(135deg,rgba(255,206,92,.30),rgba(127,163,255,.24));border:1px solid var(--border)}
.cmt-body{font-size:14px;line-height:1.65;color:var(--text);word-break:break-word;white-space:pre-line}
/* [W9-M21F] Same pairing the post body has: the renderer consumes the newlines into separate nodes,
   so a rendered comment must not ALSO get pre-line whitespace handling. Nothing is doubled today —
   the split() leaves no newline in the node — but the two bodies now share one renderer, and the day
   it starts leaving them the rule that is missing here is the one that breaks. */
.cmt-body.md{white-space:normal}
.cmt-input{display:flex;gap:8px;margin-top:12px;background:var(--surface-2);border:1px solid var(--border);border-radius:12px;padding:6px 6px 6px 13px}
.cmt-field{flex:1;background:none;border:none;outline:none;color:var(--text);font-size:14px;min-width:0}
.cmt-field::placeholder{color:var(--faint)}
.cmt-send{flex:none;font-size:13px;font-weight:800;color:var(--on-accent);background:linear-gradient(90deg,var(--accent),var(--accent-2));border-radius:9px;padding:0 16px;min-height:44px}

/* ── forms (write / cs) ──────────────────────────────────── */
.field{margin-bottom:14px}
.field-label{display:block;font-size:13px;font-weight:800;color:var(--muted);margin-bottom:6px}
.field-hint{font-weight:400;color:var(--faint)}
/* [W14-GUIDE] The board's reward rule, under the body box. It sits BELOW the field it belongs to
   (a rule read after seeing the box, not before), carries the negative margin that pulls it out of
   `.field`'s 14px gap so it reads as part of that field, and wraps — this sentence is two lines on a
   390px screen and truncating it would hide the half that says what does NOT get paid. */
.w-template-notice{margin:-8px 0 14px;font-size:12px;line-height:1.5;color:var(--faint)}
/* [ledger X-3] Live character count. It sits directly under the reward notice and inherits
   that line s bottom spacing, so the two sentences read as one block.
   The reached state is NOT signalled by colour alone: the sentence itself changes
   (i18n write.charCount.below / .reached) and colour merely reinforces it — the state must survive
   colour-vision deficiency and forced-contrast modes.
   No new colour token is invented: the palette has no success-green (measured: no --ok-family var
   in this file), and minting one here would create a colour that lives only in this rule.
   The existing --accent-text plus weight carries the contrast. */
.w-char-count{margin:-10px 0 14px;font-size:12px;line-height:1.5;color:var(--faint);font-variant-numeric:tabular-nums}
.w-char-count.reached{color:var(--accent-text);font-weight:800}
/* [W15-EARN] The general reward hint — the sibling of the two lines above, and never their neighbour:
   syncTemplate() shows exactly one of {board notice + counter} or {this}. Same size, same colour,
   same negative-top pull, so whichever one is on screen occupies the same slot under the body box and
   nothing below it moves when the selected board changes. No new token, no new colour. */
.w-reward-hint{margin:-8px 0 14px;font-size:12px;line-height:1.5;color:var(--faint)}
.field-input{width:100%;background:var(--surface-2);border:1px solid var(--border);border-radius:12px;color:var(--text);font-size:15px;padding:12px 13px;outline:none;min-height:44px}
.field-input:focus{border-color:var(--accent)}
.field-input:disabled{opacity:.65;cursor:not-allowed;color:var(--faint)}
textarea.field-input{resize:vertical;line-height:1.65}
.w-body{min-height:180px}
.cs-body{min-height:130px}
/* The stage is the scrollport, so submit remains reachable when the keyboard shortens the viewport.
   It must stay in normal flow now that both composers place a gallery picker immediately before it:
   a sticky bottom CTA visually covered the add-image control and made the feature look absent. */
/* The `background:var(--bg)` this rule used to carry is GONE. It was the leftover of the sticky dock
   described above — an opaque strip that had to hide the content scrolling under it. In normal flow
   it hides nothing and merely paints --bg (the page ground) INSIDE #app, which is --frame-bg: with
   the v2A palette those two differ enough to show as a dark band across the composer, directly under
   the attachment hint. A dead property is not free; this one was drawing. */
.submit-dock{padding:10px 0 4px}
.submit-btn{width:100%;font-size:15px;font-weight:800;color:var(--on-accent);background:linear-gradient(90deg,var(--accent),var(--accent-2));border-radius:13px;padding:14px;min-height:44px;margin-top:4px}
.submit-btn:disabled{background:var(--surface-3);color:var(--faint);cursor:not-allowed;box-shadow:none}

/* ── cs banner ───────────────────────────────────────────── */
.cs-banner{border-radius:14px;padding:14px;margin-bottom:16px;background:linear-gradient(135deg,rgba(247,198,90,.09),rgba(255,194,51,.05));border:1px solid rgba(247,198,90,.3)}
.cs-banner-t{font-size:14.5px;font-weight:800;color:var(--honest)}
.cs-banner-b{font-size:13px;color:var(--muted);margin-top:5px;line-height:1.6}

/* ── FAB ─────────────────────────────────────────────────── */
.fab{position:absolute;right:16px;bottom:calc(78px + env(safe-area-inset-bottom));z-index:30;width:54px;height:54px;border-radius:17px;background:linear-gradient(135deg,var(--accent),var(--accent-2));color:var(--on-accent);font-size:26px;line-height:1;display:flex;align-items:center;justify-content:center;box-shadow:0 8px 24px rgba(255,194,51,.4);transition:transform .12s,opacity .15s}
.fab:active{transform:scale(.92)}
.fab.hidden{opacity:0;pointer-events:none;transform:scale(.7)}

/* ── bottom tabs ─────────────────────────────────────────── */
/* [AC-18 · v2A] Five tabs, stroke icons (ui.mjs navIcon).
   The `filter:grayscale(1) opacity(.6)` that used to desaturate the emoji is GONE with the emoji:
   an SVG drawn in currentColor takes the tab's own colour, so selection is stated once — by
   `color` — instead of twice, by a colour AND a filter that could disagree. The icon box keeps a
   fixed 22px square so the five labels sit on one baseline whatever the label length. */
.tabbar{position:absolute;left:0;right:0;bottom:0;z-index:20;display:flex;background:rgba(10,14,24,.94);backdrop-filter:blur(10px);border-top:1px solid var(--border-soft);padding:6px 6px calc(8px + env(safe-area-inset-bottom))}
.tab{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;gap:3px;padding:7px 0 4px;min-height:44px;border-radius:12px;color:var(--faint);font-size:12px;font-weight:700}
.tab-ic{display:flex;align-items:center;justify-content:center;width:22px;height:22px;line-height:1}
.tab-ic svg{width:22px;height:22px;display:block}
.tab-lb{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tab.active{color:var(--accent-text)}
@media (max-width:359px){.tab{font-size:10px}.tabbar{padding-inline:3px}}

/* ── toast ───────────────────────────────────────────────── */
.toast{position:absolute;left:50%;bottom:calc(120px + env(safe-area-inset-bottom));transform:translateX(-50%) translateY(8px);z-index:60;background:#151d33;border:1px solid var(--border);color:var(--text);font-size:13px;font-weight:600;border-radius:12px;padding:10px 16px;max-width:86%;text-align:center;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;box-shadow:0 8px 30px rgba(0,0,0,.5);line-height:1.5}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}

@media (min-width:520px){#app{min-height:100dvh}}

/* [W9-M23C] CS active surface — inquiry rows + thread bubbles */
.cs-screen{display:flex;flex-direction:column;gap:14px}
.cs-list{display:flex;flex-direction:column;gap:8px}
.cs-row{display:block;width:100%;text-align:left;background:var(--surface-2);border:1px solid var(--border);border-radius:12px;padding:12px;color:var(--text);cursor:pointer}
.cs-row-head{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.cs-row-cat{font-size:12px;color:var(--muted)}
.cs-row-sum{margin-top:6px;font-size:13.5px;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cs-row-time{margin-top:4px;font-size:12px;color:var(--muted)}
.cs-thread{display:flex;flex-direction:column;gap:10px;margin:12px 0}
.cs-msg{border-radius:12px;padding:10px 12px;border:1px solid var(--border-soft)}
.cs-msg-user{background:rgba(255,194,51,.08);align-self:flex-end;max-width:88%}
.cs-msg-operator{background:rgba(247,198,90,.08);align-self:flex-start;max-width:88%}
.cs-msg-system{background:var(--surface-2);align-self:center;max-width:88%}
.cs-msg-who{font-size:12px;font-weight:700;color:var(--muted);margin-bottom:3px}
.cs-msg-body{font-size:14px;line-height:1.55;white-space:pre-wrap}
.cs-attachment-picker{margin-top:12px}
.cs-attachment-picker .attach-add{width:100%;min-height:52px;aspect-ratio:auto;flex-direction:row}
.cs-attachment-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px;margin-top:9px}
.cs-attachment-view{aspect-ratio:1;border-radius:9px;overflow:hidden;border:1px solid var(--border);background:var(--surface-2)}
.cs-attachment-view img{display:block;width:100%;height:100%;object-fit:cover}
.cs-attachment-unavailable{aspect-ratio:1;display:flex;align-items:center;justify-content:center;padding:6px;border-radius:9px;border:1px dashed var(--border);color:var(--faint);font-size:11px;text-align:center}

/* [W9-M23D] CS accessibility — counter row, ceiling notice, honest footnotes. Layout only: no new
   colour and no new variable (status tones reuse the .badge tones already shipped above). */
.field-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}
.field-head .field-label{margin-bottom:0}
.cs-count{flex:none}
.cs-count-warn:not(:empty){font-size:12.5px;line-height:1.5;color:var(--honest);margin:-6px 2px 10px}
.cs-uid-note{margin-top:10px}
.cs-first-response{margin-top:4px;font-size:12.5px;color:var(--muted)}
/* [W9-M23D] end */

/* [W9-M23E] CS input quality — category guide notice, explicit insert control, icon back button.
   The notice pair sits BELOW the enquiry box and is empty/hidden until a category change on a body the
   user has written into: no padding and no border here, so an empty notice occupies zero height and the
   deep-link viewport contract on #cs-body is untouched. .cs-back moved here from the block above: it
   used to inherit its 44px floor from .more-btn, and the icon control no longer wears that class, so the
   touch-target minimum is stated on the rule itself. */
.cs-form-hint{margin:0}
.cs-form-hint:not(:empty){font-size:12.5px;line-height:1.5;color:var(--muted);margin:-6px 2px 6px}
.cs-form-insert{min-width:44px;min-height:44px;padding:10px 14px;font-size:13px;font-weight:700;color:var(--accent-text);background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:10px}
.cs-back{min-width:44px;min-height:44px;display:inline-flex;align-items:center;justify-content:center;font-size:20px;line-height:1;color:var(--text);background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:12px}
/* [W9-M23E] end */

/* ══════════════════════════════════════════════════════════════════════════
   [W9-M21E] Community UX upgrade — appbar actions, sort, infinite scroll,
   rich editor, attachments, image viewer, notifications, profile, activity.
   Deep navy & gold premium tone; mobile-first; a11y baseline preserved. */

/* appbar action buttons (search / notifications / profile) */
.appbar-actions{flex:none;display:flex;align-items:center;gap:4px}
.appbar-act{position:relative;width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:19px;line-height:1;color:var(--text);background:transparent;border:1px solid transparent;transition:background .12s,border-color .12s}
.appbar-act:active{background:var(--surface-2);border-color:var(--border)}
.act-badge{position:absolute;top:5px;right:5px;min-width:9px;height:9px;border-radius:999px;background:var(--accent);border:1.5px solid var(--frame-bg)}
.tab-badge{position:absolute;top:4px;right:calc(50% - 16px);min-width:8px;height:8px;border-radius:999px;background:var(--accent);border:1.5px solid rgba(10,14,24,.94)}

/* quick links row (home) */
.quicklinks{display:flex;gap:8px;margin:2px 0 6px}
.qlink{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;padding:12px 4px;min-height:44px;border-radius:13px;background:var(--surface);border:1px solid var(--border-soft);color:var(--muted);font-size:12px;font-weight:700;transition:border-color .12s}
.qlink:active{border-color:var(--accent)}
.qlink-ic{font-size:19px;line-height:1}

/* sort toggle */
/* [AC-18 · v2A] The selected sort is an OUTLINE gold pill, not the filled gradient it used to share
   with the board chip. Both were filled, so a board row carried two loud gold pills stacked and
   neither said which decision it belonged to. The hierarchy is now stated: the board chip is filled
   (which room you are in) and the sort chip is outlined (how that room is ordered). */
.sortbar{display:flex;gap:6px;margin-bottom:10px;overflow-x:auto;scrollbar-width:none}
.sortbar::-webkit-scrollbar{display:none}
.sort-chip{flex:none;font-size:13px;font-weight:700;color:var(--faint);background:var(--surface);border:1px solid var(--border-soft);border-radius:999px;padding:8px 15px;min-height:44px;transition:color .12s,border-color .12s,background .12s}
.sort-chip.active{color:var(--accent-text);background:rgba(255,194,51,.09);border-color:rgba(255,194,51,.55);font-weight:800}

/* game-scoped board switcher + persisted default */
.board-picker{padding:10px;margin-bottom:12px;border-radius:14px;background:var(--surface);border:1px solid var(--border-soft)}
.board-picker-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}
.board-picker-title{font-size:13px;font-weight:900;color:var(--muted)}
.board-favorite{display:inline-flex;align-items:center;justify-content:center;gap:5px;min-height:44px;padding:0 11px;border-radius:11px;font-size:12px;font-weight:800;color:var(--faint);background:var(--surface-2);border:1px solid var(--border)}
.board-favorite.active{color:var(--boost);border-color:rgba(255,194,51,.55);background:rgba(255,194,51,.08)}
.board-favorite:disabled{opacity:.55}
.board-chips{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none}
.board-chips::-webkit-scrollbar{display:none}
.board-chip{flex:none;min-height:44px;padding:0 14px;border-radius:999px;font-size:13px;font-weight:800;color:var(--faint);background:var(--frame-bg);border:1px solid var(--border)}
.board-chip.active{color:var(--on-accent);background:linear-gradient(90deg,var(--accent),var(--accent-2));border-color:transparent}
/* [OWNER 2026-07-29] Event proof boards now appear in this row, grouped behind the ordinary ones.
   The separator is a LABEL, not a rule line: a bare divider in a horizontally scrolling row reads as
   "the list ended" once it reaches the fold, and the chips after it would look like a second widget.
   The event chips keep the same shape as every other chip (same height, same radius) — being an
   event board changes where a chip sits, not what kind of control it is. `.active` still wins,
   because "the board you are on" outranks "which group it belongs to". */
.board-chip-group{flex:none;align-self:center;padding:0 4px 0 6px;font-size:12px;font-weight:800;letter-spacing:.04em;color:var(--faint);white-space:nowrap}
.board-chip.event{color:var(--honest);border-color:rgba(247,198,90,.34);background:rgba(247,198,90,.06)}

/* post-card stats + thumbnail strip */
/* [AC-18 · v2A] The `.post-stats` / `.post-stat` pair that used to sit here is DELETED, not edited:
   it was written for the icon+number shape (gap:3px, centre-aligned around a 💬 glyph) and it lost
   to the identical selectors in the post-card block above, which is where the row is defined now.
   Two rules with the same selector means the earlier one is dead — this file has been bitten by
   exactly that before (see .detail-gallery img) and you only find out from the screen. */
.post-thumb-strip{display:flex;gap:6px;margin-top:9px}
.post-thumb{width:56px;height:56px;border-radius:9px;object-fit:cover;background:var(--surface-2);border:1px solid var(--border-soft)}

/* infinite scroll: sentinel + skeleton + spinner */
.scroll-sentinel{height:1px}
.skeleton{border-radius:13px;background:var(--surface);border:1px solid var(--border-soft);padding:13px;margin-bottom:8px;overflow:hidden}
.sk-line{height:12px;border-radius:6px;margin:6px 0;background:linear-gradient(90deg,var(--surface-2) 25%,var(--surface-3) 37%,var(--surface-2) 63%);background-size:400% 100%;animation:sk 1.4s ease infinite}
.sk-line.w70{width:70%}.sk-line.w40{width:40%}
@keyframes sk{0%{background-position:100% 0}100%{background-position:0 0}}
@media (prefers-reduced-motion:reduce){.sk-line{animation:none}}
.load-spin{display:flex;justify-content:center;padding:14px}
.load-dot{width:22px;height:22px;border-radius:999px;border:2.5px solid var(--border);border-top-color:var(--accent);animation:spin .7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.load-dot{animation:none}}

/* [Explicit replacement — W9-UX4] .editor-toolbar / .tool-btn rules deleted with the toolbar they
   dressed (see screens/write.mjs). A rule with no node left to match is stale, and leaving it would
   also leave a false signal that the composer still has secondary controls to audit.
   An earlier draft of this comment claimed .tool-btn "carried the only 44px touch-target floor in the
   composer" — that was FALSE and REFUTE caught it. `.field-input` and `.submit-btn` both carry
   `min-height:44px` and both predate this order; measured after the deletion, every composer control
   is still >=44px (select 48 / title 46 / body 180 / submit 48). What .tool-btn uniquely carried was
   min-WIDTH. A false cause written into the record is worse than no comment. */

/* attachments (upload / preview / remove) */
.attach-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:8px}
.attach-cell{position:relative;aspect-ratio:1;border-radius:11px;overflow:hidden;background:var(--surface-2);border:1px solid var(--border-soft)}
.attach-cell img{width:100%;height:100%;object-fit:cover;display:block}
.attach-cell.uploading{display:flex;align-items:center;justify-content:center}
.attach-rm{position:absolute;top:4px;right:4px;width:26px;height:26px;border-radius:999px;display:flex;align-items:center;justify-content:center;font-size:15px;line-height:1;color:#fff;background:rgba(5,7,13,.72);border:1px solid var(--border)}
.attach-add{aspect-ratio:1;border-radius:11px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;font-size:12px;font-weight:700;color:var(--faint);background:var(--surface-2);border:1px dashed var(--border);min-height:44px}
.attach-add:active{border-color:var(--accent);color:var(--accent-text)}
.attach-add-ic{font-size:22px;line-height:1}
.attach-add:disabled{opacity:.55;cursor:not-allowed}
.attach-hint{font-size:12px;color:var(--faint);margin-top:8px;line-height:1.5}
/* The empty composer needs a visible gallery CTA before any thumbnail exists. A square first tile
   was 100px tall and the sticky submit dock covered its label on a 390px viewport. Make only the
   lone add tile a full-width 52px row; once images exist, the four-column thumbnail grid remains. */
.write-form .attach-grid>div:only-child{grid-column:1/-1}
.write-form .attach-grid>div:only-child .attach-add{width:100%;min-height:52px;aspect-ratio:auto;flex-direction:row}

/* image viewer (fullscreen) */
.viewer{position:absolute;inset:0;z-index:80;background:rgba(3,4,8,.94);display:flex;align-items:center;justify-content:center;padding:16px;animation:vfade .16s ease}
@keyframes vfade{from{opacity:0}to{opacity:1}}
@media (prefers-reduced-motion:reduce){.viewer{animation:none}}
.viewer img{max-width:100%;max-height:100%;object-fit:contain;border-radius:8px}
.viewer-close{position:absolute;top:calc(12px + env(safe-area-inset-top));right:14px;width:44px;height:44px;border-radius:999px;display:flex;align-items:center;justify-content:center;font-size:22px;color:#fff;background:rgba(5,7,13,.6);border:1px solid var(--border)}
.viewer-msg{font-size:13.5px;color:var(--muted);text-align:center;max-width:30ch}

/* [W11-EVENT] detail: event proof entry — the ONLY door to an event's proof board, since those
   boards are hidden from the boards list on purpose. It is full-width and 44px+ because it is the
   primary action of an announcement article, and it sits above the stats row where the reader
   finishes the article. Colours come from the existing variables only (no new custom property —
   T-THEME variable contract). The OPEN state borrows the accent; UPCOMING/ENDED stay muted so a
   closed event never looks like a live call to action. */
.proof-wrap{margin:16px 0 4px}
.proof-entry{display:flex;align-items:center;gap:8px;width:100%;text-align:left;font-size:14.5px;font-weight:800;color:var(--muted);background:var(--surface-2);border:1px solid var(--border);border-radius:13px;padding:13px 15px;min-height:44px;transition:color .12s,border-color .12s,background .12s}
/* [OWNER 2026-07-29] The OPEN treatment moved to the button-hierarchy block at the foot of this file
   (outline chip -> filled Primary). It is NOT restated here: two homes for one decision means one of
   them is dead, and this file has been bitten by exactly that before (see .detail-gallery img). The
   press state moved there too, with every other control's. */
.proof-entry-ic{flex:none}
.proof-entry-lb{flex:1 1 auto}

/* detail: action row (bookmark / edit / report) + gallery + stats */
.detail-actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0 4px}
.act-chip{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:700;color:var(--muted);background:var(--surface-2);border:1px solid var(--border);border-radius:999px;padding:9px 15px;min-height:44px;transition:color .12s,border-color .12s,background .12s}
.act-chip:active{border-color:var(--accent)}
.act-chip.on{color:var(--accent-text);border-color:var(--accent);background:rgba(255,194,51,.12)}
.act-chip.done{color:var(--faint);pointer-events:none}
.detail-gallery{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin:12px 0}
/* [W9-M21G] `.detail-gallery img` was a DEAD rule until this order put an <img> in the gallery, and
   reviving it split the tile's look across two homes at the same specificity as `.attach-cell img`
   (0,1,1). Measured: the 4/3 was already inert (both w and h set), and the img drew its own 1px
   border + 11px radius INSIDE the tile's — a double frame. The tile owns the frame; the image just
   fills it. Same lesson as signV5 in this order: two homes for one decision means one of them is
   dead, and you find out from the screen. */
.detail-gallery .attach-cell{aspect-ratio:4/3}
.official-media{position:relative;display:block;width:100%;aspect-ratio:24/5;overflow:hidden;background:var(--surface-2);border:1px solid var(--border-soft);border-radius:12px}
.official-media-header{margin:12px 0 18px}
.official-media-footer{margin:18px 0 12px}
.official-media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.official-media-placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:22px;color:var(--faint)}
.official-media:disabled{cursor:default}
/* The glyph is a placeholder UNDER the image, so it must not take part in flow — measured before
   this: glyph and img pushed each other, the glyph overhung the tile by 3px, and the image jumped
   22px the moment it loaded. Both are pinned to the tile box instead. */
.attach-cell>.attach-add-ic{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
.attach-cell>img{position:absolute;inset:0}
/* [AC-18 · v2A] Mirrors the board list's stat row exactly — same label-then-number shape, same
   accent on the reaction count. The two rows describe the same three facts about the same post, so
   a reader who learned to read one has already learned the other. */
.detail-stats{display:flex;gap:16px;margin:12px 2px 4px;font-size:12px;color:var(--faint)}
.detail-stat{display:inline-flex;align-items:baseline;gap:5px}
.detail-stat-lb{color:var(--faint)}
.detail-stat-n{font-weight:800;color:var(--muted);font-variant-numeric:tabular-nums}
.detail-stat-reactions .detail-stat-lb,.detail-stat-reactions .detail-stat-n{color:var(--accent-text)}

/* notifications */
.notif-row{display:flex;align-items:flex-start;gap:11px;padding:13px;width:100%;text-align:left;background:var(--surface);border:1px solid var(--border-soft);border-radius:13px;margin-bottom:8px}
.notif-row.unread{border-color:var(--accent);background:rgba(255,194,51,.07)}
.notif-ic{width:34px;height:34px;flex:none;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:16px;background:var(--surface-2);border:1px solid var(--border)}
.notif-main{flex:1;min-width:0}
.notif-text{font-size:14px;line-height:1.5;color:var(--text)}
.notif-time{font-size:12px;color:var(--faint);margin-top:3px}
.notif-dot{width:8px;height:8px;flex:none;border-radius:999px;background:var(--accent);margin-top:6px}

/* profile */
.profile-id{font-size:12px;color:var(--faint);margin-top:2px;font-variant-numeric:tabular-nums;word-break:break-all}
/* [AC-18 · v2A] 3 -> 4 columns: the enquiry entry joined this row when the CS tab left the bar
   (profile.mjs).
   Four across is still 44px+ wide at 320px (the narrowest supported width), so the row stays one
   line rather than wrapping a single orphan tile onto a second. */
.profile-shortcuts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;margin-bottom:12px}
.profile-shortcut{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;min-height:72px;padding:8px 4px;border-radius:13px;font-size:12px;font-weight:800;color:var(--muted);background:var(--surface);border:1px solid var(--border-soft)}
.profile-shortcut:active{border-color:var(--accent)}
.profile-shortcut-ic{font-size:19px;line-height:1}
.nickname-gate{min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--bg)}
.nickname-gate-inner{width:100%;max-width:390px}
.nickname-gate-inner>h2{text-align:center;font-size:22px;margin:10px 0 18px}
.nickname-gate-icon{text-align:center;font-size:38px}
.nickname-intro{color:var(--muted);font-size:14px;line-height:1.6;margin-bottom:14px}
.nickname-actions{display:grid;grid-template-columns:minmax(110px,.8fr) minmax(0,1.2fr);gap:8px;margin-top:10px}
.secondary-btn{min-height:44px;border-radius:12px;padding:10px 12px;border:1px solid var(--accent);color:var(--accent-text);font-weight:800;background:var(--surface-2)}
.secondary-btn:disabled{opacity:.55}
.nickname-actions .submit-btn{margin-top:0;padding:10px}
.nickname-check-status{min-height:20px;margin-top:9px;color:var(--muted);font-size:13px;line-height:1.5}
.profile-name-readonly{margin-top:6px;padding:13px;border-radius:12px;background:var(--surface-2);border:1px solid var(--border);font-weight:800}

/* activity tabs */
.subtabs{display:flex;gap:6px;margin-bottom:12px}
.subtab{flex:1;font-size:13px;font-weight:700;color:var(--faint);background:var(--surface);border:1px solid var(--border-soft);border-radius:11px;padding:11px 0;min-height:44px;transition:color .12s,border-color .12s}
.subtab.active{color:var(--accent-text);border-color:var(--accent);background:rgba(255,194,51,.08)}
.status-pill{font-size:11px;font-weight:700;color:var(--faint);border:1px solid var(--border);border-radius:999px;padding:2px 8px;margin-left:6px}

/* badge tone (neutral) */
.badge.neutral{color:var(--faint);border-color:var(--border);background:var(--surface-2)}

/* Boostshop */
.shop-screen{padding-bottom:8px}
.shop-game-picker{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px;padding:11px 12px;border:1px solid var(--border-soft);border-radius:13px;background:var(--surface);font-size:12.5px;font-weight:800;color:var(--muted)}
.shop-game-select{min-width:0;max-width:68%;height:40px;padding:0 30px 0 11px;border:1px solid rgba(255,194,51,.36);border-radius:11px;color:var(--text);background:var(--surface-2);font-size:13px;font-weight:800}
.shop-loading{padding:30px;text-align:center;color:var(--muted)}
/* ★[AC-18 · v2A] THE WHOLE WALLET WAS BELOW THE CAPTION FLOOR. Measured before this pass: the point
   NAME was 9px, the order badge 9px, the spend-order sentence 10px — on the one screen in the app
   where money changes hands. It survived because spa-a11y audits home/list/detail/write/cs and the
   shop was simply not on that list, so nothing was ever asserting about it; the same order that adds
   the type scale here adds `auditScreen(page, 'shop')` to that spec, because a size nobody measures
   goes back down.
   The v2A treatment (bigger value, name above it, where-it-works line under it) is what makes 12px
   affordable: the boxes got taller, not narrower. */
.shop-wallet{margin-bottom:11px;padding:13px;border:1px solid rgba(255,194,51,.30);border-radius:16px;background:linear-gradient(135deg,rgba(255,194,51,.12),rgba(127,163,255,.06) 62%,transparent)}
.shop-wallet-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-bottom:10px;font-size:13px;font-weight:800;color:var(--muted)}
.shop-wallet-head span{color:var(--boost);font-weight:900;font-size:17px;font-variant-numeric:tabular-nums}
.shop-point-row{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px}
.shop-point{position:relative;display:flex;flex-direction:column;gap:2px;min-width:0;padding:24px 8px 9px;border:1px solid var(--border-soft);border-radius:12px;text-align:left;background:var(--surface)}
.shop-point-order{position:absolute;top:7px;left:8px;width:18px;height:18px;border-radius:50%;font-size:12px;font-weight:900;line-height:18px;text-align:center;color:var(--on-accent);background:var(--boost)}
.shop-point-name{display:block;overflow:hidden;color:var(--muted);font-size:12px;font-weight:800;white-space:nowrap;text-overflow:ellipsis}
.shop-point strong{font-size:16px;line-height:1.2;color:var(--text);font-variant-numeric:tabular-nums}
/* Where this kind can be spent (home.points.*.sub, read not copied — screens/shop.mjs). It WRAPS on
   purpose: the warp line is two lines in a third of a 390px screen, and truncating it would cut the
   half that says the points are usable somewhere else at all. */
.shop-point-sub{font-size:12px;line-height:1.35;color:var(--faint)}
.shop-waterfall{margin:9px 0 0;color:var(--muted);font-size:12px;line-height:1.5}
.shop-notice{margin-bottom:10px;padding:11px 13px;border-radius:12px;color:var(--muted);font-size:13px;line-height:1.5;background:rgba(247,198,90,.08);border:1px solid rgba(247,198,90,.28)}
.shop-notice.locked{background:rgba(255,194,51,.08);border-color:rgba(255,206,92,.25)}
.shop-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.shop-card{position:relative;display:flex;flex-direction:column;min-width:0;padding:9px;border:1px solid var(--border-soft);border-radius:14px;background:var(--surface)}
.shop-card.tier-premium{background:linear-gradient(135deg,rgba(247,198,90,.13),var(--surface) 55%);border-color:rgba(247,198,90,.48)}
/* The thumb IS the placeholder and the <img> lies on top of it, so the three-tier icon chain
   (display.image_url → /assets/shop/{package_id}.png → this box) needs no swap and no reflow: an
   image that fails to resolve removes itself and the tile it was covering is already drawn. */
.shop-thumb{position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden;width:100%;aspect-ratio:16/10;border-radius:10px;background:var(--surface-2);border:1px solid var(--border-soft)}
.shop-image{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}

/* [OWNER 2026-07-29] Boostshop identity — two product families, and a differentiated image-less tile.
   This block is NOT a stand-in until artwork arrives. Measured 2026-07-29: all nine active products
   have image_url null, and null is the normal state. So the placeholder varies by (family × tier):
   painting a whole family one colour would make the three products inside it identical again, which
   is the defect being repaired.
   No new colour token — every value is an alpha composite of the existing variables
   (--accent / --accent-2 / --boost / --playboost / --warpboost), dark tone preserved (§6 C1).

   [OWNER 2026-07-29 · SECOND PASS] The two sections became two TABS and the charge card lay down.
   Why the section headings are gone rather than restyled: with a tab strip on screen, a heading
   repeating the tab's own label under it is the same word twice, and it cost a whole line above the
   fold. The tab strip reuses .subtabs/.subtab wholesale — that component already carries the four
   interaction states from the block at the bottom of this file, so a new tab class would have been a
   fifth control that silently opts out of hover/press/focus. */
.shop-tabs{margin-bottom:10px}
.shop-tab{display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 6px}
.shop-tab-ic{font-size:15px;line-height:1}
.shop-panel-note{margin:0 2px 9px;font-size:12.5px;line-height:1.55;color:var(--muted)}
/* The charge tab's opening statement. Louder than a note because it corrects a belief (points that
   arrived free still count as a real top-up), and quieter than a card because it is not a product. */
.shop-banner{margin:0 0 10px;padding:12px 13px;border-radius:13px;font-size:12.5px;font-weight:700;line-height:1.55;color:var(--text);background:linear-gradient(135deg,rgba(255,194,51,.14),rgba(255,194,51,.09));border:1px solid rgba(255,194,51,.34)}
.shop-card.kind-charge{border-color:rgba(255,194,51,.3)}

/* Charge card, lying down. One per row: icon · (name + figures) · CTA. Measured against the portrait
   card it replaces (~215px tall in a 3-up grid) this lands near 70px — the order asked for half or
   less. The CTA keeps min-height:44px from .shop-cta, which is what sets the row's floor. */


.shop-row{flex-direction:row;align-items:center;gap:10px;padding:8px 10px}
.shop-row .shop-thumb{flex:none;width:52px;height:52px;aspect-ratio:auto;border-radius:11px;font-size:24px}
.shop-row-main{flex:1;min-width:0}
.shop-row .shop-name{margin:0;font-size:13.5px;-webkit-line-clamp:1}
.shop-row-figs{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 8px;margin-top:2px}
.shop-row .shop-price{margin:0;padding:0;font-size:13px}
.shop-row .shop-parity{margin:0;font-size:12px}

/* Placeholder = gradient + oversized glyph, with the angle and density moving per tier. */
.shop-thumb.placeholder{font-size:34px;text-shadow:0 2px 10px rgba(0,0,0,.45)}
.ph-charge{border-color:rgba(255,194,51,.34)}
.ph-charge-low{background:linear-gradient(145deg,rgba(255,194,51,.20),rgba(255,143,79,.16) 70%,var(--surface-2))}
.ph-charge-mid{background:linear-gradient(115deg,rgba(255,194,51,.28),rgba(255,194,51,.18) 62%,var(--surface-2))}
.ph-charge-high{background:linear-gradient(85deg,rgba(255,194,51,.36),rgba(255,194,51,.22) 58%,var(--surface-2))}
.ph-charge-premium{background:linear-gradient(60deg,rgba(255,194,51,.46),rgba(255,206,92,.24) 54%,var(--surface-2))}
.ph-package{border-color:rgba(255,206,92,.26)}
.ph-package-low{background:linear-gradient(145deg,rgba(255,194,51,.20),rgba(26,36,64,.9) 70%,var(--surface-2))}
.ph-package-mid{background:linear-gradient(115deg,rgba(255,194,51,.28),rgba(61,220,132,.10) 64%,var(--surface-2))}
.ph-package-high{background:linear-gradient(85deg,rgba(195,166,255,.26),rgba(255,194,51,.24) 58%,var(--surface-2))}
.ph-package-premium{background:linear-gradient(60deg,rgba(247,198,90,.34),rgba(195,166,255,.22) 54%,var(--surface-2))}

/* Won parity on a charge card: directly under the price and quieter than it — the price is the
   canonical figure and this line is only its translation. */
.shop-parity{margin:1px 0 7px;font-size:12px;line-height:1.35;color:var(--muted);font-variant-numeric:tabular-nums}
.shop-badges{display:flex;flex-wrap:wrap;align-items:center;gap:5px;margin-top:7px}
/* [AC-18 · v2A] The tier chip carries its own TONE and everything about it is derived from that one
   value. Before, the base chip was accent-coloured and only PREMIUM had an override — which, with a
   gold accent, made every tier look premium. Four tiers now read apart at a glance the way v2A draws
   them (plain · info · warp-purple · gold), and the chip's tint, border and halo are all composed
   from `--tier-tone` rather than restated three times each. The glow below reads the same variable,
   so a fifth tier needs ONE line here and nothing in the motion block. 9px -> 12px is the caption
   floor (§2.6) — see the wallet note above for why the shop escaped it. */
.shop-tier{--tier-tone:var(--muted);padding:3px 8px;border-radius:999px;font-size:12px;font-weight:900;line-height:1.4;color:var(--tier-tone);background:color-mix(in srgb,var(--tier-tone) 12%,transparent);border:1px solid color-mix(in srgb,var(--tier-tone) 36%,transparent)}
.tier-mid .shop-tier{--tier-tone:var(--info)}
.tier-high .shop-tier{--tier-tone:var(--warpboost)}
.tier-premium .shop-tier{--tier-tone:var(--boost)}
/* [OWNER 2026-07-29 · game-team table] Reward visibility on an item-mall card. The ratio is the one
   number that sells the product, so it gets the only filled badge on the tile; the composition and
   the worth line under the name are the evidence for it. Every figure is a literal from the curated
   table — this file cannot compute them and neither can the screen. */
.shop-ratio{padding:3px 9px;border-radius:999px;font-size:12px;font-weight:900;line-height:1.4;white-space:nowrap;font-variant-numeric:tabular-nums;color:var(--bg);background:linear-gradient(90deg,var(--boost),var(--honest))}
/* The item-mall tile is a 2:1 band rather than the 16:10 tile the 3-up grid used. Its card now
   carries four lines of reward evidence, and those lines are what the product is sold on — a tall
   decorative crop above them buys nothing and costs a whole card of scroll every two products. */
.shop-panel-package .shop-thumb{aspect-ratio:2/1}
.shop-parts{margin:6px 0 0;font-size:12px;line-height:1.45;color:var(--text)}
.shop-worth{margin:3px 0 0;font-size:12px;line-height:1.45;color:var(--boost);font-weight:800;font-variant-numeric:tabular-nums}
.shop-appeal{align-self:flex-start;margin-top:6px;padding:3px 8px;border-radius:8px;font-size:12px;font-weight:900;line-height:1.4;color:var(--accent-text);background:rgba(255,194,51,.14);border:1px solid rgba(255,206,92,.28)}
.shop-name{display:-webkit-box;overflow:hidden;margin:8px 0 0;font-size:13.5px;font-weight:800;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2}
.shop-description{display:none}
.shop-price{margin:auto 0 7px;padding-top:8px;font-size:13px;font-weight:900;color:var(--boost);font-variant-numeric:tabular-nums}
/* [OWNER 2026-07-29] 36px -> 44px and 10px -> 12px. This is the PRIMARY action of the shop screen and
   it was under both baselines (§2.6 touch target and caption size) — it simply sat outside the audited
   screens, so nothing said so. Wrapping to two lines inside the 44px floor is fine and intended: the
   confirm label is longer than the idle one and must not be squeezed instead of wrapped. */
.shop-cta{width:100%;min-height:44px;padding:8px 4px;border-radius:9px;font-size:12px;line-height:1.25;font-weight:900;color:var(--on-accent);background:linear-gradient(90deg,var(--accent),var(--accent-2))}
.shop-cta:disabled{color:var(--faint);background:var(--surface-3)}
/* The row card's CTA shrinks to its content instead of the card width — but it stays BELOW the base
   rule on purpose. `.shop-cta{` is a substring of `.shop-row .shop-cta{`, and an override placed
   above the base makes a first-match extractor read the override's body as the base rule's (that is
   exactly how UIUX ③-g went red on the first draft). min-height:44px is inherited untouched. */
.shop-row .shop-cta{flex:none;width:auto;min-width:78px;padding:8px 12px}
.shop-success{display:flex;min-height:60vh;flex-direction:column;align-items:center;justify-content:center;text-align:center}
.shop-success-icon{font-size:42px}
.shop-success h2{margin:14px 0 5px;font-size:20px}
.shop-success p{margin:0;color:var(--muted);font-size:13px}
.shop-request{margin-top:18px;padding:13px 16px;border-radius:11px;font-size:12px;word-break:break-all;color:var(--muted);background:var(--surface-2);border:1px solid var(--line)}
/* The <=339px narrowing is gone with the 3-up grid it narrowed: the item mall is 2-up everywhere and
   the charge list is 1-up everywhere, so there is nothing left for a breakpoint to rescue. */

/* [W9-UX3] The .banner-ph rule is gone with its last consumer (home). It reserved 16/6 of the screen
   width — 136px at 390px — for an asset that does not exist, directly above the content the user came
   for. A style with no node is stale by definition; when the OWNER asset arrives it brings its own. */

/* ══════════════════════════════════════════════════════════════════════════
   [OWNER 2026-07-29 · game-team design request] INTERACTION STATES, UNIFIED.

   The report was that nothing answers the finger: a card, a chip and a button all looked identical
   pressed and unpressed, and a screen's primary action wore the same outline as its neighbours.

   ★ Why this block is HERE and not spread over the components. A state rule that lives beside one
     component is a rule the next component forgets — measured in this very file before this order:
     `:active{border-color:var(--accent)}` had been hand-copied onto six row classes and onto none of
     the chips, so a chip press was invisible while a row press was not, for no stated reason.

   ★ Hover is fenced behind `(hover:hover) and (pointer:fine)` and that fence is load-bearing. On a
     touch screen `:hover` LATCHES after a tap and stays until the next tap lands elsewhere, so an
     unfenced hover rule paints a permanent "selected" state onto the last thing the user touched.
     That is worse than having no hover at all, and it is invisible to a desktop-only review.

   ★ The press is a transform, and the transform is parameterised (--press) so that reduced motion can
     drop the MOVEMENT while keeping the colour feedback. A user who asked for less motion still has
     to be told the tap registered.

   ★ `:not(:disabled)` is on the hover arm rather than a reset afterwards. A reset would have to
     restore each property to a per-component value, and the first component whose idle border differs
     from the reset's guess would silently acquire a hover-coloured border while disabled.

   No new colour: hover borrows --accent, the focus ring --accent-text, the press is geometry only.

   [OWNER 2026-07-29 · motion pass 2] .98 -> .96. At .98 a 44px control moves under a millimetre — on a
   phone that is inside the thickness of the finger that caused it, so the feedback was arriving
   where nobody could see it. .96 is still short of a "bounce" and the reduced-motion arm below
   still cancels the movement entirely. */
:root{--press:scale(.96)}
@media (prefers-reduced-motion:reduce){:root{--press:none}}

.feed-row,.notice-row,.board-row,.post-card,.cs-row,.notif-row,.qlink,.profile-shortcut,.proof-entry,
.sort-chip,.board-chip,.act-chip,.subtab,.more-btn,.link-btn,.appbar-act,.appbar-back,.board-favorite,
.cs-form-insert,.cs-back,.attach-add,.secondary-btn,.tab,.viewer-close,.attach-rm,
.submit-btn,.cmt-send,.appbar-close,.fatal-close,.shop-cta,.btn-primary{
  transition:background-color .16s ease,border-color .16s ease,color .16s ease,
             box-shadow .16s ease,filter .16s ease,transform .11s ease;
}

@media (hover:hover) and (pointer:fine){
  .feed-row:not(:disabled):hover,.notice-row:not(:disabled):hover,.board-row:not(:disabled):hover,
  .post-card:not(:disabled):hover,.cs-row:not(:disabled):hover,.notif-row:not(:disabled):hover,
  .qlink:not(:disabled):hover,.profile-shortcut:not(:disabled):hover,.proof-entry:not(:disabled):hover,
  .sort-chip:not(:disabled):hover,.board-chip:not(:disabled):hover,.act-chip:not(:disabled):hover,
  .subtab:not(:disabled):hover,.more-btn:not(:disabled):hover,.link-btn:not(:disabled):hover,
  .board-favorite:not(:disabled):hover,.cs-form-insert:not(:disabled):hover,.cs-back:not(:disabled):hover,
  .attach-add:not(:disabled):hover,.secondary-btn:not(:disabled):hover,.tab:not(:disabled):hover{
    border-color:var(--accent);
    /* The brightness rise is a FILTER, not a `color` declaration. `color` here would be (0,3,0) and
       would therefore outrank every `.x.active{color:…}` in this file — hovering the selected sort
       chip or the current tab would erase the very signal that says it is selected. A filter lifts
       text and surface together and fights nobody. */
    filter:brightness(1.12);
    box-shadow:0 2px 12px rgba(0,0,0,.30);
  }
  /* The app-bar actions and the overlay controls are chrome, not content: they lift by surface, not
     by accent, so the bar never grows a row of gold outlines under a travelling cursor. */
  .appbar-act:not(:disabled):hover,.appbar-back:not(:disabled):hover,
  .viewer-close:not(:disabled):hover,.attach-rm:not(:disabled):hover{
    background-color:var(--surface-2);border-color:var(--border);
  }
  /* Filled controls have no border to move, so they brighten instead. */
  .submit-btn:not(:disabled):hover,.cmt-send:not(:disabled):hover,.appbar-close:not(:disabled):hover,
  .fatal-close:not(:disabled):hover,.shop-cta:not(:disabled):hover,.btn-primary:not(:disabled):hover,
  .fab:not(:disabled):hover,.rec-btn:not(:disabled):hover{filter:brightness(1.12)}
}

/* Press. Kept apart from hover on purpose: a phone never reaches the hover arm, and this is the only
   feedback a thumb gets. `.fab` and `.rec-btn` keep their own louder presses (.92 / .96). */
.feed-row:not(:disabled):active,.notice-row:not(:disabled):active,.board-row:not(:disabled):active,
.post-card:not(:disabled):active,.cs-row:not(:disabled):active,.notif-row:not(:disabled):active,
.qlink:not(:disabled):active,.profile-shortcut:not(:disabled):active,.proof-entry:not(:disabled):active,
.sort-chip:not(:disabled):active,.board-chip:not(:disabled):active,.act-chip:not(:disabled):active,
.subtab:not(:disabled):active,.more-btn:not(:disabled):active,.appbar-act:not(:disabled):active,
.appbar-back:not(:disabled):active,.board-favorite:not(:disabled):active,
.cs-form-insert:not(:disabled):active,.cs-back:not(:disabled):active,.attach-add:not(:disabled):active,
.secondary-btn:not(:disabled):active,.tab:not(:disabled):active,.viewer-close:not(:disabled):active{
  transform:var(--press);
}
.submit-btn:not(:disabled):active,.cmt-send:not(:disabled):active,.appbar-close:not(:disabled):active,
.fatal-close:not(:disabled):active,.shop-cta:not(:disabled):active,.btn-primary:not(:disabled):active{
  transform:var(--press);filter:brightness(1.12);
}

/* Focus ring. Specificity is deliberately (0,1,0) via :where() so it neither outranks nor is outranked
   by a component rule at the same weight — it simply comes later, which is what makes it win over the
   `outline:none` on .field-input while leaving that element's :focus border rule alone. */
:where(a,button,select,input,textarea,summary):focus-visible{
  outline:2px solid var(--accent-text);
  outline-offset:2px;
}

/* Disabled. :where() again keeps this at (0,1,0), so every component that already states its own
   disabled treatment (.field-input .65 / .attach-add .55 / .board-favorite .55 / .submit-btn's muted
   palette + not-allowed) keeps it, and only controls with NO statement fall back here. The existing
   `not-allowed` declarations are left as they are: they are a deliberate earlier choice and they read
   as "unavailable" more loudly than `default` does. */
:where(button,select,input,textarea):disabled{cursor:default;opacity:.6}

/* ── Button hierarchy ─────────────────────────────────────────────────────
   Primary = filled gradient (.btn-primary and the four hand-copied twins .submit-btn / .cmt-send /
   .appbar-close / .fatal-close, plus .shop-cta and the FAB). Secondary keeps its outline.
   The one control that was in the WRONG tier is the event proof entry: it is the primary action of an
   announcement article — the single door into that event's proof board — and it shipped as an outline
   chip indistinguishable from the bookmark/report row beneath it. OPEN is promoted to the filled
   treatment; UPCOMING and ENDED stay muted, because a closed event must never look like a live call
   to action. */
.proof-entry.open{
  color:var(--on-accent);
  background:linear-gradient(90deg,var(--accent),var(--accent-2));
  border-color:rgba(255,206,92,.32);
  box-shadow:0 4px 14px rgba(0,0,0,.24);
}
/* The count is a FACT ABOUT the destination, not part of the instruction to go there. Folded into the
   label it made the button's accessible name change every time somebody else posted, and it pushed
   the verb ("go and prove it") into the middle of a sentence. It is now a badge on the right. */
.proof-entry-n{
  flex:none;
  padding:3px 10px;
  border-radius:999px;
  font-size:12px;
  font-weight:800;
  line-height:1.4;
  white-space:nowrap;
  font-variant-numeric:tabular-nums;
  color:var(--muted);
  background:var(--surface);
  border:1px solid var(--border);
}
.proof-entry.open .proof-entry-n{color:var(--on-accent);background:rgba(5,7,13,.22);border-color:rgba(5,7,13,.34)}

/* ══════════════════════════════════════════════════════════════════════════
   [OWNER 2026-07-29 · motion pass 2, axes A+B+C] MOTION, UNIFIED.

   Everything here is written from scratch against the existing palette. No third-party CSS was
   imported and no new colour token exists — every glow is an alpha composite of --accent /
   --accent-2 / --accent-text / --boost, which is what keeps the dark tone intact (§6 C1).

   ★ THE MOBILE WEBVIEW IS THE CANONICAL SURFACE, so nothing here hangs off :hover. A phone never
     produces a hover, and on a touch screen :hover LATCHES after a tap — a hover-driven sweep would
     fire on the last thing the user touched and stay. The two triggers that exist on a phone are
     "it just appeared" (a one-shot on mount) and "a finger is on it" (:active). Ambient loops carry
     the rest.

   ★ WHY GLOWS ARE PSEUDO-ELEMENTS THAT ONLY ANIMATE opacity. Animating box-shadow means the shadow
     is re-rasterised every frame — on a low-end webview that is where the frames go. A pseudo-element
     with a STATIC shadow is painted once and promoted; animating its opacity is a compositor-only
     change. So: static shadow on ::after, opacity on the timeline, never the other way round.

   ★ DURATIONS. Interaction and reveal motion lives in the 150–250ms band (--mo-in / --mo-sweep).
     The ambient loops (breathing glow, pulse dot, flow line) are NOT responses to an action and are
     deliberately slow — a 200ms breath is a strobe, not a breath. They are the only exceptions and
     they are named here so the exception is visible rather than discovered.

   ★ EVERY animation below has a reduced-motion arm, collected in ONE block at the end. A stagger
     without that arm is the worst case of all: the items start at opacity 0 and, with the animation
     suppressed but the fill kept, they never arrive. The arm therefore turns animations OFF (so the
     natural, final state applies) rather than shortening them. */
:root{
  --mo-in:200ms;        /* list reveal — one shot, in band */
  --mo-sweep:240ms;     /* card light sweep — one shot, in band */
  --mo-step:34ms;       /* per-item stagger step */
  --mo-ease:cubic-bezier(.22,.61,.36,1);
  --mo-i:0;             /* reveal index — set by the ladder below, consumed by calc() */
}

/* ── Stagger ladder ───────────────────────────────────────────────────────
   ★ ONE index, MANY animations. CSS cannot turn :nth-child(n) into a number, so the ladder is
     written out once — and it is written for EVERY position, not for the second and third item.
     A "2·3 only" stagger breaks at the fourth row, which is the first row most catalogues actually
     have. Past the cap the step stops growing on purpose: an item cannot be worth a third of a
     second of delay just for being twelfth, and paginated lists append past it.
   ★ Because --mo-i is a custom property it INHERITS, so a pseudo-element of a staggered child can
     compute its own delay off the same index (the card sweep does exactly that). That is the whole
     reason the index is a variable instead of a hard-coded delay per rule. */
.bv-reveal>*:nth-child(1){--mo-i:0}
.bv-reveal>*:nth-child(2){--mo-i:1}
.bv-reveal>*:nth-child(3){--mo-i:2}
.bv-reveal>*:nth-child(4){--mo-i:3}
.bv-reveal>*:nth-child(5){--mo-i:4}
.bv-reveal>*:nth-child(6){--mo-i:5}
.bv-reveal>*:nth-child(7){--mo-i:6}
.bv-reveal>*:nth-child(8){--mo-i:7}
.bv-reveal>*:nth-child(9){--mo-i:8}
.bv-reveal>*:nth-child(n+10){--mo-i:9}
.bv-reveal>*{
  animation:bv-rise var(--mo-in) var(--mo-ease) both;
  animation-delay:calc(var(--mo-i) * var(--mo-step));
}
@keyframes bv-rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

/* ── A · Primary CTA breathing glow ───────────────────────────────────────
   The three primary calls to action (shop exchange / event-proof entry / write FAB). The pseudo carries no
   background, only a shadow, so it never covers the button's own gradient — it is a halo around it.
   A disabled CTA is excluded: an unavailable control that breathes is an invitation to press a
   thing that cannot be pressed. */
.shop-cta:not(:disabled),.proof-entry.open,.fab{position:relative}
.shop-cta:not(:disabled)::after,.proof-entry.open::after,.fab::after{
  content:'';
  position:absolute;
  inset:0;
  border-radius:inherit;
  pointer-events:none;
  box-shadow:0 0 8px 1px rgba(255,194,51,.55),0 0 0 1px rgba(255,206,92,.30);
  opacity:.28;
  animation:bv-breathe 2400ms ease-in-out infinite;
}
.fab::after{box-shadow:0 0 14px 3px rgba(255,194,51,.55),0 0 0 1px rgba(255,206,92,.30)}
@keyframes bv-breathe{0%,100%{opacity:.22}50%{opacity:.62}}

/* The arrow is the only label part that can move without fighting the press transform (which owns
   `transform` on the button itself). A 3px round trip is a nudge, not a slide. */
.proof-entry.open .proof-entry-ic{display:inline-block;animation:bv-nudge 2200ms var(--mo-ease) infinite}
@keyframes bv-nudge{0%,70%,100%{transform:translateX(0)}82%{transform:translateX(3px)}}

/* ── B · Boostshop ────────────────────────────────────────────────────────
   Light sweep. NOT hover-driven: it plays once as the card arrives (offset behind that card's own
   reveal by one step, so the light follows the card instead of racing it) and again under a finger.
   The replay needs a DIFFERENT animation-name — re-declaring the same name with the same values does
   not restart a running or finished animation, so a same-name :active rule would look like nothing
   happened. The keyframes are shared; only the handle differs. */
.shop-card{overflow:hidden}
.shop-card::before{
  content:'';
  position:absolute;
  inset:0;
  z-index:2;
  pointer-events:none;
  background:linear-gradient(105deg,transparent 34%,rgba(255,206,92,.16) 47%,transparent 60%);
  transform:translateX(-125%);
  animation:bv-sweep var(--mo-sweep) var(--mo-ease) both;
  animation-delay:calc(var(--mo-i) * var(--mo-step) + 90ms);
}
.shop-card:active::before{animation:bv-sweep-again var(--mo-sweep) var(--mo-ease) both;animation-delay:0ms}
@keyframes bv-sweep{from{transform:translateX(-125%)}to{transform:translateX(125%)}}
@keyframes bv-sweep-again{from{transform:translateX(-125%)}to{transform:translateX(125%)}}

/* Tier badge glow — same pseudo/opacity construction as the CTA halo, one octave quieter. */
.shop-tier{position:relative}
.shop-tier::after{
  content:'';
  position:absolute;
  inset:-1px;
  border-radius:inherit;
  pointer-events:none;
  box-shadow:0 0 7px 1px color-mix(in srgb,var(--tier-tone) 55%,transparent);
  opacity:0;
  animation:bv-halo 2800ms ease-in-out infinite;
}
/* No per-tier override here any more: the halo composes the SAME --tier-tone the chip does, so the
   two can never disagree about which family a tier belongs to. */
@keyframes bv-halo{0%,100%{opacity:.12}50%{opacity:.72}}

/* Pulse dot on the charge tab. The family marker used to be a pill on every card and the pulse
   belonged to it; the pill retired into the tab, so the pulse followed the label rather than being
   left on a node that no longer exists. */
.shop-tab[data-shop-tab=charge] .shop-tab-ic{position:relative}
.shop-tab[data-shop-tab=charge] .shop-tab-ic::after{
  content:'';
  position:absolute;
  top:-2px;
  right:-5px;
  width:5px;
  height:5px;
  border-radius:50%;
  background:var(--boost);
  animation:bv-dot 1800ms ease-in-out infinite;
}
@keyframes bv-dot{0%,100%{opacity:.32;transform:scale(.82)}50%{opacity:1;transform:scale(1)}}

/* Spend-order flow line. A light segment crosses the three point boxes left to right — which is
   literally what the sentence under it says happens to your points (1 → 2 → 3). Transform only. */
.shop-point-row{position:relative}
.shop-point-row::after{
  content:'';
  position:absolute;
  left:0;
  bottom:-4px;
  width:32%;
  height:1px;
  pointer-events:none;
  background:linear-gradient(90deg,transparent,rgba(255,194,51,.6),transparent);
  animation:bv-flow 2600ms var(--mo-ease) infinite;
}
@keyframes bv-flow{0%{transform:translateX(-20%)}70%,100%{transform:translateX(320%)}}

/* ── C · In-progress event ────────────────────────────────────────────────
   The dot goes ONLY on the entry whose phase the server actually derived as OPEN. The events list
   and the home event feed carry no phase on the wire — they are "recent posts on the event board",
   which includes finished events. A live-pulse there would be a liveness claim nobody made. */
.proof-entry.open .proof-entry-lb{position:relative}
.proof-entry.open .proof-entry-lb::after{
  content:'';
  position:absolute;
  top:2px;
  margin-left:5px;
  width:5px;
  height:5px;
  border-radius:50%;
  background:var(--on-accent);
  animation:bv-dot 1800ms ease-in-out infinite;
}

/* ── Reduced motion — the single arm for everything above ─────────────────
   Turned OFF, not shortened. `bv-rise` fills backwards from opacity:0, so an arm that merely made
   it fast would still leave a terminal that honours the fill sitting on invisible rows. With the
   animation gone the elements are simply in their natural state, which is the finished state.
   The ambient glows keep a static, quieter opacity so the hierarchy they carry does not vanish. */
@media (prefers-reduced-motion:reduce){
  .bv-reveal>*,
  .shop-card::before,
  .shop-card:active::before,
  .shop-point-row::after,
  .proof-entry.open .proof-entry-ic{animation:none}
  .shop-card::before{opacity:0}
  .shop-cta:not(:disabled)::after,.proof-entry.open::after,.fab::after{animation:none;opacity:.3}
  .shop-tier::after{animation:none;opacity:.35}
  .shop-tab[data-shop-tab=charge] .shop-tab-ic::after,
  .proof-entry.open .proof-entry-lb::after{animation:none;opacity:1}
}

/* ── [W12-CLAIM] open-launch claim modal ─────────────────────────────────────
   z-index 50: above the fab (30) and tabbar (20) — the modal must own the screen — but BELOW the
   toast host (60), so a toast raised by anything else is still legible while this is open. Its own
   failures are announced INSIDE the card (.claim-note) for the same reason, only inverted: a message
   about THIS action must not be the one thing hidden behind THIS overlay.
   No history entry is pushed (unlike .viewer at 80) — see claim.mjs for why that guard is not extended.
   Colours are existing tokens only (§6 C1 — no new custom property): --text on --surface-2, --muted
   for the deferral, --danger for the failure line, and the shared .btn-primary fill for the CTA
   (--on-accent on the gold gradient, the same 11.9:1 pair .submit-btn ships).
   The 44px floors are load-bearing: spa-a11y audits HOME and this overlay is on screen there. */
.claim-overlay{position:absolute;inset:0;z-index:50;background:rgba(3,4,8,.82);display:flex;align-items:center;justify-content:center;padding:20px;animation:vfade .16s ease}
@media (prefers-reduced-motion:reduce){.claim-overlay{animation:none}}
.claim-card{width:100%;max-width:340px;padding:22px 20px 18px;border-radius:18px;background:var(--surface-2);border:1px solid rgba(255,194,51,.34);box-shadow:0 18px 48px rgba(0,0,0,.55);text-align:center}
.claim-title{margin:0 0 10px;font-size:17px;line-height:1.35;font-weight:900;color:var(--boost)}
/* ONE pack key holds both promo lines; the newline in it renders here. */
.claim-body{margin:0;font-size:13.5px;line-height:1.6;font-weight:600;color:var(--text);white-space:pre-line}
.claim-note{margin:10px 0 0;font-size:12.5px;line-height:1.5;font-weight:700;color:var(--danger);min-height:0}
.claim-note:empty{display:none}
.claim-actions{display:flex;flex-direction:column;gap:8px;margin-top:18px}
.claim-cta{width:100%;min-width:44px;min-height:48px;padding:0 16px;border-radius:14px;font-size:15px;font-weight:900}
.claim-later{width:100%;min-width:44px;min-height:44px;padding:0 16px;border-radius:12px;font-size:13px;font-weight:700;color:var(--muted);background:transparent;border:1px solid var(--border)}

/* ── [W18-DASH · sian v2A] shop — coupon top tier at the mockup weight ───── */
/* The charge grid is NOT touched: samez-shop2 ①-i pins it to one column of horizontal rows as a
   deliberate product decision (portrait tiles were removed there on purpose). The mockup wants a
   multi-column charge grid; that is a reversal of a pinned decision, not a skin, so it is raised
   for the master rather than taken here. */
.shop-card.tier-premium{background:linear-gradient(135deg,rgba(247,198,90,.16),var(--surface) 55%);border-color:rgba(247,198,90,.52)}

/* ── [W18-VERBATIM · OWNER 2026-08-15 design-verbatim order] charge tiles, two columns ───── */
/* ★★ EXPLICIT SUPERSESSION of the one-column charge panel (samez-uiux ①-i). That rule was a product
   judgement made BEFORE the mockup existed — portrait tiles were dropped because they padded height.
   The owner's design-verbatim order outranks it, and the mockup lays charges out as a compact grid.
   ■ Why TWO columns and not the mockup's three: the mockup's tile is legible only at 8.5–10.5px, and
     this SPA's a11y floor is 12px (a floor, not a budget). At 12px three columns overflow a 390px
     frame — measured. Two is therefore the densest layout that is BOTH the mockup's shape and legible,
     i.e. the ceiling of design-verbatim under the floor, not a compromise away from it.
   ■ What is NOT superseded: the row DOM (`shop-card shop-row`), the absent tier pill, the absent
     description, and the 44px CTA floor all stand — those were about card HEIGHT, and none of them is
     what put charges in one column. The base `.shop-row` stays a horizontal row for every other
     panel; only the charge panel restacks. */
.shop-panel-charge .shop-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}
.shop-panel-charge .shop-row{flex-direction:column;align-items:stretch;gap:6px;padding:10px 8px 8px;border-radius:13px}
/* The mockup's charge tile carries no artwork — the won figure IS the identity. The node stays in the
   DOM (loader + fallback shared with every other card); only its box leaves the flow. */
.shop-panel-charge .shop-row .shop-thumb{display:none}
.shop-panel-charge .shop-row-main{display:flex;flex-direction:column;align-items:center;gap:2px;text-align:center}
.shop-panel-charge .shop-name{font-size:14.5px;font-weight:800;letter-spacing:-.01em;font-variant-numeric:tabular-nums}
.shop-panel-charge .shop-row-figs{display:flex;flex-direction:column;align-items:center;gap:1px}
.shop-panel-charge .shop-price{font-size:12.5px;font-weight:800;color:var(--boost);font-variant-numeric:tabular-nums}
.shop-panel-charge .shop-parity{font-size:12px;color:var(--faint);font-variant-numeric:tabular-nums}
.shop-panel-charge .shop-row .shop-cta{width:100%}
.shop-card.kind-charge{border-color:rgba(255,194,51,.34)}

/* ── [W18-VERBATIM] coupon cards — the mockup's weight on the top tier ───── */
.shop-card.tier-premium{background:linear-gradient(135deg,rgba(247,198,90,.16),var(--surface) 55%);border-color:rgba(247,198,90,.52)}

/* ── [W18-VERBATIM] board list — the mockup keeps the card, adds the press feedback ───── */
/* ★ Nothing here overrides a SIZE. The list card already carries the mockup geometry (14px radius,
   soft border, navy ground, 15px title) — measured before writing this, and re-stating those values
   would only create a second home for them. What the mockup has and this file did not is the press
   state and the gold wash on operator posts, so that is all that lands. */
.post-card:active{background:var(--surface-2)}
.post-card.official-post{background:linear-gradient(135deg,rgba(255,194,51,.09),var(--surface) 58%);border-color:rgba(255,194,51,.30)}

/* [W18-TRAINC 2R] The settled-PREVIOUS-period row. Never on the same line as the live leaderboard
   above it: folding them together prints two claims about two periods as one sentence ("final" over
   numbers that are still moving). 12px is the a11y caption floor — this row is secondary. */
.rank-hold{font-size:12px;font-weight:800;letter-spacing:.02em;color:var(--text);opacity:.85}
.rankb-settled{display:flex;align-items:center;gap:8px;margin:6px 0 2px}
.rankb-settled-t{font-size:12px;font-weight:700;color:var(--muted)}
