/* ============================================================================
   CORE — identical in every product. Do not edit per product.
   ============================================================================
   Everything metric is shared; everything hued is not. A game and a tax app
   should not look the same — they should look like they were built by the same
   people. So the type scale, the radii, the shadow geometry, the frame and the
   token NAMES are fixed here, and each product supplies only colour, below the
   PALETTE banner.

   The naming rule, which is the whole naming policy:

       the CSS variable is the Tailwind token path with dots as dashes.
       line.strong <-> --line-strong.  ink.2 <-> --ink-2.  brand.500 <-> --brand-500.

   It ends every naming argument and it is checkable by eye. Before it, sets
   broke it in both directions at once — Tailwind key `line` with CSS var
   `--border`, key `ink` with `--text-primary` — and neither name could be
   trusted to find the other.
   ============================================================================ */

:root {
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Type. The first four steps were byte-identical across every set that had
     a token layer — the strongest existing convention in the tree. */
  --fs-2xs: 10px;     --lh-2xs: 1.3;
  --fs-xs: 11.5px;    --lh-xs: 1.4;
  --fs-sm: 13px;      --lh-sm: 1.45;
  --fs-base: 14px;    --lh-base: 1.5;
  --fs-body: 16px;    --lh-body: 1.5;
  --fs-lead: 18px;    --lh-lead: 1.45;
  --fs-h3: 22px;      --lh-h3: 1.25;    --ls-h3: -0.01em;
  --fs-h2: 28px;      --lh-h2: 1.15;    --ls-h2: -0.02em;
  --fs-h1: 36px;      --lh-h1: 1.08;    --ls-h1: -0.02em;
  --fs-display: 46px; --lh-display: 1.02; --ls-display: -0.03em;

  --tracking-label: 0.18em;

  --radius-sm: 8px;
  --radius: 12px;
  --radius-lg: 16px;
  --radius-xl: 20px;
  --radius-2xl: 24px;
  --radius-3xl: 28px;
  --radius-pill: 999px;
  --radius-phone: 40px;

  /* Geometry is shared; tint and strength are per product, because a dark
     palette needs far more alpha to read at all. --shadow-tint is an `r g b`
     triple, declared in the palette block. */
  --shadow-a1: 0.05;
  --shadow-a2: 0.07;
  --shadow-a3: 0.18;
  --shadow-sm: 0 1px 2px rgb(var(--shadow-tint) / var(--shadow-a1));
  --shadow:    0 4px 14px rgb(var(--shadow-tint) / var(--shadow-a2));
  --shadow-lg: 0 14px 34px -10px rgb(var(--shadow-tint) / var(--shadow-a3));
  --shadow-phone: 0 0 0 1px var(--line-strong), 0 30px 60px -20px rgb(var(--shadow-tint) / var(--shadow-a3));

  --frame-w: 390px;
  --frame-h: 844px;
}

html, body {
  font-family: var(--font-sans);
  background: var(--page);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  margin: 0;
}

.mono { font-family: var(--font-mono); }
.num  { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; letter-spacing: -0.01em; }

/* ---- Phone frame — a device, not a utility --------------------------------
   390 wide is the spec and never changes. The HEIGHT is clamped to whatever is
   left under the chrome bar, and the content scrolls inside the frame the way
   it scrolls on the phone.

   This replaces a transform: scale() approach that looked right and was not —
   a transform does not change layout height, so the frame still occupied its
   full 844px, and the page scrolled to reveal a frame that was already drawn
   smaller. --chrome-h is measured by _chrome.js, because the bar wraps to two
   rows on a narrow window and a guessed constant is wrong exactly when it
   matters. */
.phone {
  width: var(--frame-w);
  max-width: 100%;
  height: min(var(--frame-h), calc(100dvh - var(--chrome-h, 0px) - var(--stage-pad, 32px)));
  background: var(--frame);
  /* The frame re-resolves ink itself. Without this, text with no colour class
     inherits body's COMPUTED colour, which was resolved in the light scope —
     so a dark frame kept black body text while everything token-driven around
     it turned white. A variable remap only reaches declarations that read the
     variable; inheritance carries the old value, not the new name. */
  color: var(--ink);
  display: flex;
  flex-direction: column;
  position: relative;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-radius: var(--radius-phone);
  box-shadow: var(--shadow-phone);
}
.phone::-webkit-scrollbar { width: 0; }
/* A footer pinned to the bottom of the DEVICE, not the bottom of the content:
   sticky keeps it in view while the content above it scrolls, which is what a
   tab bar or a primary action actually does on the phone. */
.phone > .pin-bottom { margin-top: auto; position: sticky; bottom: 0; }

/* The stage is exactly the frame plus its padding, so the page itself never
   scrolls on a screen that already fits. */
.stage {
  display: grid;
  place-items: start center;
  /* The same variable the frame subtracts from its own height, so the two
     cannot disagree. A literal 16px here left the frame 32px shorter than the
     space it was given. */
  padding: calc(var(--stage-pad, 32px) / 2) 0;
  height: 100dvh;
  box-sizing: border-box;
}

/* ?bare is what the screenshots sheet embeds and what you capture a single
   screen with. The gutter and the device rounding are framing for a screen
   being *looked at*; inside a 390x844 tile they are a white margin and four
   clipped corners, and the frame no longer fills the tile it was sized for. */
[data-bare] .stage { padding: 0; }
[data-bare] .phone {
  border-radius: 0;
  box-shadow: none;
  max-width: none;
}

/* A theme scope is a token remap, never a second set of utilities. A utility
   written against a variable follows whichever scope it lands in, so the theme
   toggle changes no markup and nothing can be dark in one place and light in
   another. */
/* A screen with a modal over it does not scroll. `position: absolute; inset: 0`
   resolves against the frame's padding box, and in a scroll container that is
   the visible box pinned to the top of the scrollable area — so scrolling the
   frame slid the backdrop off the screen and left the card floating over
   nothing. A modal blocks the content underneath in the app too. */
.phone--modal { overflow: hidden; }

.phone--page   { background: var(--page); }
.phone--sunken { background: var(--sunken); }

/* The device status bar. The OS draws this, not the app — but the app pads for
   it (pt-safe), so a wireframe that omits it puts every screen ~44px too high
   and makes the first row look like it has more air than it will. */
.statusbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: none;
  padding: 12px 20px 2px;
  font-size: 13px;
  color: var(--ink);
}
.statusbar-time { font-weight: 600; letter-spacing: 0.02em; }
.statusbar-icons { display: flex; align-items: center; gap: 6px; }

/* ---- Dev chrome — injected by _chrome.js, not part of the product ---------
   Everything around the screen and nothing in it. One bar, same controls, same
   order, in every product.

   And the same COLOURS in every product. These are declared here rather than
   read from the palette because the bar is tooling: reading --brand-500 made
   the active chip blue in tic-tac-toe and navy in charades, so the same
   control looked like a different control depending on which set you opened,
   and the bar read as part of the product rather than as the thing holding
   it. Nothing below this line may reference a product token. */
:root {
  --chrome-bg:      #ffffff;
  --chrome-surface: #ffffff;
  --chrome-line:    #d4d4d8;   /* zinc-300 */
  --chrome-ink:     #3f3f46;   /* zinc-700 */
  --chrome-ink-2:   #71717a;   /* zinc-500 */
  --chrome-ink-3:   #a1a1aa;   /* zinc-400 */
  --chrome-active:  #18181b;   /* zinc-900 */
  --chrome-soft:    #f4f4f5;   /* zinc-100 */
}

.chrome {
  position: sticky;
  top: 0;
  z-index: 50;
  border-bottom: 1px solid var(--chrome-line);
  background: color-mix(in srgb, var(--chrome-bg) 92%, transparent);
  backdrop-filter: blur(8px);
  padding: 7px 16px;
}
.chrome-row {
  max-width: 1200px;
  margin: 0 auto;
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 6px;
  font-size: 11px;
}
.chrome-group { display: flex; align-items: center; gap: 6px; }
/* The middle group scrolls; the row does not wrap. A wrapped row is two
   stacked bars, which reads as two bars — chitragupt's settings flow has
   twenty-three screens and pushed the bar onto a second line on a full-width
   window. Scrolling the names keeps the row one line at every width, so the
   nav on the left and the display controls on the right never move, and
   --chrome-h stays put instead of doubling. min-width:0 is what actually lets
   a flex child shrink below its content and scroll. */
.chrome-mid {
  flex: 0 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.chrome-mid::-webkit-scrollbar { display: none; }
.chrome-left, .chrome-right { flex: 0 0 auto; }
/* Navigation left, settings right, and a gap between them that grows. */
.chrome-right { margin-left: auto; }
/* There is no .chrome-label. The breadcrumb it styled — `mobile · /game ›` —
   said what the surface toggle on the right already says and what the flow
   tint in the chart already says, and it sat between the nav chips and the
   screen chips pushing the two apart for no information. */
.chrome-chip {
  border-radius: 6px;
  border: 1px solid var(--chrome-line);
  background: var(--chrome-surface);
  padding: 2px 8px;
  color: var(--chrome-ink);
  text-decoration: none;
  font: inherit;
  cursor: pointer;
  white-space: nowrap;
}
.chrome-chip:hover { border-color: var(--chrome-active); }
.chrome-chip.is-current {
  border-color: var(--chrome-active);
  background: var(--chrome-soft);
  color: var(--chrome-active);
  font-weight: 600;
}

/* Icon controls ------------------------------------------------------------
   The controls that mean the same thing on every page — all routes,
   screenshots, back, next, theme, surface, notes — are icons. They are lucide,
   the estate's one icon set, asked for by name: nothing here carries a copy of
   any path data, and there is no second icon library on the page.

   The chip keeps its label in `title` and `aria-label`, so hovering still says
   which control this is. An icon whose meaning you have to click to learn is
   worse than the word it replaced. */
.chrome-chip--icon {
  width: 24px;
  height: 22px;
  padding: 0;
  display: inline-grid;
  place-items: center;
}
.chrome-ico { display: grid; place-items: center; line-height: 0; }
.chrome svg { width: 14px; height: 14px; }
/* The fallback when the lucide CDN did not load: the placeholder renders its
   own label instead of an empty square. Wider than an icon slot, so the chip
   grows to fit rather than clipping the word. */
.chrome-ico--text {
  font-size: 9px;
  font-weight: 700;
  font-style: normal;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1;
  padding: 0 4px;
  white-space: nowrap;
}
.chrome-chip--icon:has(.chrome-ico--text) { width: auto; padding: 0 4px; }
.seg-chip--icon:has(.chrome-ico--text) { padding: 2px 6px; }

/* Segmented controls: surface, theme, and this screen's states. Always
   rendered in the same slot — a control that appears only sometimes moves
   everything beside it the day a product gains a surface or a dark scope. */
.seg {
  display: inline-flex;
  border: 1px solid var(--chrome-line);
  border-radius: 999px;
  overflow: hidden;
}
.seg--states { margin-left: 4px; }
.seg-chip {
  padding: 2px 10px;
  font: inherit;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--chrome-ink-2);
  background: var(--chrome-surface);
  text-decoration: none;
  border: 0;
  cursor: pointer;
  white-space: nowrap;
}
.seg-chip + .seg-chip { border-left: 1px solid var(--chrome-line); }
.seg-chip:hover { color: var(--chrome-active); }
.seg-chip.is-current { background: var(--chrome-active); color: #fff; }
.seg-chip--icon { padding: 2px 8px; display: inline-grid; place-items: center; }

/* A surface that is not drawn is struck through, never omitted: a missing
   thing has to look missing, or its absence reads as "no such thing". */
.seg-chip.is-absent { color: var(--chrome-ink-3); text-decoration: line-through; cursor: default; }
/* line-through does not cross an <svg>, so an icon half draws its own strike:
   a hairline at the same angle, in the same colour, so a struck icon and a
   struck word read as the same statement. It applies to a dead back/next as
   well as to an undrawn theme or surface — §4 says the ends of the set are
   struck through, and dimming alone did not say it. */
.seg-chip--icon.is-absent,
.chrome-chip--icon.is-absent { position: relative; text-decoration: none; }
.seg-chip--icon.is-absent::after,
.chrome-chip--icon.is-absent::after {
  content: "";
  position: absolute;
  left: 5px;
  right: 5px;
  top: 50%;
  border-top: 1px solid currentColor;
  transform: rotate(-20deg);
}
/* A dead back/next at the ends of the set: shown, not hidden, so the bar does
   not reflow as you step through it. */
.chrome-chip.is-absent { color: var(--chrome-ink-3); border-color: var(--chrome-line); cursor: default; }
.chrome-chip.is-absent:hover { border-color: var(--chrome-line); }

/* ---- Notes drawer --------------------------------------------------------
   The rationale lives in the wiki and only in the wiki. This renders it beside
   the screen on demand, collapsed by default — the screen is the thing under
   review, and an essay next to it competes for the eye.

   Dev chrome, so the same fixed colours as the bar. */
.notes {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: min(400px, 92vw);
  z-index: 60;
  display: flex;
  flex-direction: column;
  background: var(--chrome-surface);
  border-left: 1px solid var(--chrome-line);
  box-shadow: -12px 0 32px rgb(24 24 27 / 0.12);
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--chrome-ink);
}
/* A panel that covers a third of the screen needs a way out that is not
   "find the button you opened it with", which the bar may have scrolled. */
.notes-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--chrome-line);
  flex: none;
}
.notes-title {
  flex: 1;
  margin: 0;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  font-weight: 700;
  color: var(--chrome-ink-2);
}
.notes-close {
  flex: none;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border: 1px solid var(--chrome-line);
  border-radius: 7px;
  background: var(--chrome-surface);
  color: var(--chrome-ink);
  font: inherit;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
.notes-close:hover { background: var(--chrome-soft); border-color: var(--chrome-active); }
.notes-body { flex: 1; min-height: 0; overflow-y: auto; padding: 16px 18px 28px; }
.notes-body h3, .notes-body h4, .notes-body h5 {
  font-size: 13px; color: var(--chrome-active); margin: 16px 0 6px;
}
.notes-body h3:first-child, .notes-body h4:first-child { margin-top: 0; }
.notes-body p { margin: 0 0 10px; }
.notes-body ul { margin: 0 0 10px; padding-left: 18px; }
.notes-body li { margin-bottom: 4px; }
.notes-body code {
  font-family: var(--font-mono); font-size: 11.5px;
  background: var(--chrome-soft); padding: 1px 4px; border-radius: 4px;
}
.notes-body a { color: var(--chrome-active); }

/* The info button lives at the far right of the bar, past the display
   controls: it is about the page you are on, and it is the only control that
   opens something over the screen rather than changing the screen. */
/* The notes button is a .chrome-chip--icon like the rest; it only differs in
   being round, because it is the one control that opens something OVER the
   screen rather than changing which screen you are looking at. */
.chrome-info { border-radius: 999px; margin-left: 2px; }
.chrome-info.is-current { background: var(--chrome-active); border-color: var(--chrome-active); color: #fff; }
/* A screen with no wiki page still renders the button, struck out, for the
   same reason a missing theme is struck out rather than omitted: the gap has
   to look like a gap. */
.chrome-info.is-absent { color: var(--chrome-ink-3); cursor: default; }

/* There is no .screen-layout and no .rationale any more. One screen per file:
   a second frame in a file has no URL, cannot appear in routes.js, and cannot
   be linked to. Themes are the toggle in the chrome bar, states are their own
   files, and the reasoning is in the wiki behind the notes drawer. */

/* ---- The flow chart ------------------------------------------------------
   The gallery is this and nothing else: no title, no description, no tables.
   React Flow draws it; the node positions come from routes.js. */
/* The chart fills what is left under the bar. _chrome.js measures the bar and
   publishes --chrome-h, so this stays right when it wraps to two rows. */
.chart-page { height: 100dvh; display: flex; flex-direction: column; }
.chart-canvas { flex: 1; min-height: 0; background: var(--page); }

/* React Flow paints its own node chrome; this replaces it rather than sitting
   on top, so the nodes look like the rest of the estate. */
.react-flow__node.fnode {
  width: auto;
  padding: 10px 14px 10px 12px;
  border: 1px solid var(--line);
  /* The flow's colour, on the edge a reader's eye follows down a column. */
  border-left: 4px solid var(--line-strong);
  border-radius: 10px;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  font-family: var(--font-sans);
  opacity: 0.88;
  display: flex;
  align-items: center;
  gap: 12px;
}
.react-flow__node.fnode:hover { border-color: var(--brand-500); opacity: 1; }
/* The normal way through the product is the thing a flow chart has to answer
   first, so the screens on it are the ones that read as solid. */
.react-flow__node.fnode.is-main { opacity: 1; box-shadow: var(--shadow); }
/* The two ends say so: where a session starts, and where it finishes. */
.react-flow__node.fnode.is-start { box-shadow: 0 0 0 2px var(--brand-500), var(--shadow); }
.react-flow__node.fnode.is-end   { box-shadow: 0 0 0 2px var(--success), var(--shadow); }
/* Hovering a screen isolates its own paths: everything it does not touch fades
   back. With thirty edges on one chart this is the difference between tracing
   a route and guessing at one. Declared after .is-main deliberately — same
   specificity, and the dim has to win over the spine's own opacity. */
.react-flow__node.fnode.is-dim { opacity: 0.16; }
.react-flow__node.fnode.is-lit {
  opacity: 1;
  border-color: var(--brand-500);
  box-shadow: 0 0 0 2px var(--brand-500), var(--shadow);
}
/* The screen whose routes are being traced, as against the screens that lead
   to it — otherwise "how do I reach X" lights up a dozen boxes and X is not
   one of them to look at. */
.react-flow__node.fnode.is-sel {
  border-color: var(--brand-700);
  background: var(--brand-50);
  box-shadow: 0 0 0 3px var(--brand-500), var(--shadow-lg);
}
.fnode-label { display: flex; flex-direction: column; gap: 2px; text-align: left; }
/* The box asks the chart a question; only this opens the screen. Without a
   separate control there was no way to interrogate a node without leaving the
   page you were interrogating it from. */
.fnode-go {
  flex: none;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  margin-left: auto;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--frame);
  color: var(--ink-3);
}
.fnode-go:hover { background: var(--brand-500); border-color: var(--brand-500); color: #fff; }
.fname { font-size: 13px; font-weight: 700; color: var(--ink); line-height: 1.2; }
.fflow { font-size: 9.5px; color: var(--ink-3); letter-spacing: 0.02em; }
.react-flow__handle { opacity: 0; }

/* The key. Six swatches, so the colours mean something without a paragraph. */
.react-flow__panel.fkey {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 12px;
  max-width: 260px;
  margin: 12px;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  box-shadow: var(--shadow-sm);
  font-size: 10px;
  color: var(--ink-2);
}
.fkey-item { display: inline-flex; align-items: center; gap: 5px; }
.fkey-dot { width: 9px; height: 3px; border-radius: 2px; }
.fkey-line { width: 14px; height: 0; border-top: 2px solid var(--brand-500); }
.fkey-toggle {
  flex: 0 0 100%;
  margin-bottom: 2px;
  padding: 3px 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: var(--surface);
  font: inherit;
  font-weight: 700;
  color: var(--ink-2);
  cursor: pointer;
}
.fkey-toggle.is-on { background: var(--brand-500); border-color: var(--brand-500); color: #fff; }
.fkey-hint { flex: 0 0 100%; margin-top: 2px; color: var(--ink-4); font-size: 9.5px; }

.react-flow__controls-button {
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  fill: var(--ink-2);
}

/* ---- Gallery -------------------------------------------------------------
   The route table IS the screens inventory. A second inventory in Markdown is
   a copy that drifts the day a screen is added. */
.route-table { width: 100%; font-size: 13px; border-collapse: collapse; }
.route-table th {
  text-align: left;
  padding: 9px 13px;
  font-size: 10px;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-3);
  border-bottom: 1px solid var(--line);
  background: var(--muted);
}
.route-table td {
  padding: 9px 13px;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.route-table tr:last-child td { border-bottom: none; }

.section-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--ink-3);
  font-weight: 700;
}

/* Status of a screen, on its gallery row and nowhere else. A screen must not
   move sections the day it is approved. */
.status { font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 6px; white-space: nowrap; }
.status--built    { background: var(--success-bg); color: var(--success-fg); }
.status--approved { background: var(--muted);      color: var(--ink-2); }
.status--review   { background: var(--warn-bg);    color: var(--warn-fg); }

.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }

/* lucide injects an <svg> in place of the <i> and inherits currentColor. */
.lucide { display: inline-block; vertical-align: -0.125em; stroke-width: 2; }

/* ---- a11y baseline -------------------------------------------------------- */
button:focus-visible,
a:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}

/* ============================================================================
   PALETTE — DwarSeva. The only block that differs between products.
   ============================================================================
   ONE VOCABULARY FOR BOTH SURFACES. This block is identical in designs/mobile
   and designs/web; only the components below it differ.

   RESERVED — two colours carry product meaning and may not be spent elsewhere:

     1. The brand gradient (brand-600 -> accent-500, orange into fuchsia) is the
        logo and the website's ONE primary sell: "View Demo", "Get Started".
        It never decorates.
     2. The four status colours are a visitor's, a bill's and a member's STATE,
        the same in every role's app:
          warn    = waiting on someone  (PENDING, PENDING_APPROVAL, IN_REVIEW)
          info    = allowed, not done   (APPROVED, PARTIALLY_PAID, TRIAL)
          success = done / inside / ok  (CHECKED_IN, PAID, ACTIVE)
          danger  = refused or late     (REJECTED, OVERDUE, SUSPENDED)
        See wiki/concepts/status-colours.md.

   WHERE THE VALUES COME FROM. The apps declare no theme: apps/mobile's
   tailwind.config.js extends nothing and both Next apps' globals.css is a bare
   `@import "tailwindcss"`. So every value here is the stock Tailwind colour the
   source names — gray-900 for the mobile primary button, blue-600/indigo-600
   for the app mark and the admin console, orange/fuchsia for the website — and
   the feedback set is the hex table in apps/mobile/src/components/ui/Badge.tsx.

   KNOWN DRIFT, recorded rather than fixed:
     - The website writes shadcn role classes (bg-primary, text-muted-foreground,
       bg-secondary, bg-card) that no @theme defines, so in the app they paint
       nothing. The set draws what the markup asks for: primary = brand,
       muted-foreground = ink-3, secondary = muted. wiki/decisions records it.
     - The product has two identities. The website and its style-guide page say
       orange/fuchsia; the mobile app and the admin console say blue/indigo over
       gray-900. `brand` is the declared one; `shell` is what the apps ship.
     - No dark palette. apps/mobile/app.json ships userInterfaceStyle
       "automatic" and no screen carries a dark class. Listed in routes.js.
   ============================================================================ */

:root {
  --shadow-tint: 17 24 39;       /* gray-900 */
  --shadow-a1: 0.05;
  --shadow-a2: 0.07;
  --shadow-a3: 0.16;

  /* Brand · orange — the website's primary, per apps/website style-guide page. */
  --brand-50:  #FFF7ED;
  --brand-100: #FFEDD5;
  --brand-200: #FED7AA;
  --brand-400: #FB923C;
  --brand-500: #F97316;
  --brand-600: #EA580C;
  --brand-700: #C2410C;
  --brand-900: #7C2D12;

  /* Accent · fuchsia — the far end of the brand gradient. RESERVED, see above. */
  --accent-50:  #FDF4FF;
  --accent-100: #FAE8FF;
  --accent-200: #F5D0FE;
  --accent-400: #E879F9;
  --accent-500: #D946EF;
  --accent-600: #C026D3;
  --accent-700: #A21CAF;
  --accent-900: #701A75;

  /* Shell · what the two apps actually paint their identity with: the shield
     tile's blue -> indigo gradient and the admin console's active nav. */
  --shell:      #4F46E5;          /* indigo-600 */
  --shell-from: #2563EB;          /* blue-600 */
  --shell-bg:   #EEF2FF;          /* indigo-50 */

  /* Text */
  --ink:   #111827;               /* gray-900 — also the mobile primary button */
  --ink-2: #374151;               /* gray-700 */
  --ink-3: #6B7280;               /* gray-500 */
  --ink-4: #9CA3AF;               /* gray-400 */
  --ink-inverse: #FFFFFF;

  /* Surfaces */
  --page:    #F3F4F6;             /* gray-100 */
  --frame:   #FFFFFF;
  --surface: #FFFFFF;
  --muted:   #F9FAFB;             /* gray-50 */
  --sunken:  #F3F4F6;             /* gray-100 */

  /* Borders */
  --line:        #E5E7EB;         /* gray-200 */
  --line-strong: #D1D5DB;         /* gray-300 */

  /* Status — RESERVED, see above. bg/fg are Badge.tsx's hex pairs. */
  --success: #10B981; --success-bg: #DCFCE7; --success-fg: #166534;
  --warn:    #F59E0B; --warn-bg:    #FEF3C7; --warn-fg:    #92400E;
  --danger:  #EF4444; --danger-bg:  #FEE2E2; --danger-fg:  #991B1B;
  --info:    #3B82F6; --info-bg:    #DBEAFE; --info-fg:    #1E40AF;

  /* Product · NOTICE CATEGORY — the two announcement categories that are not a
     status: EVENTS and POLICY (categoryConfig in announcements.tsx). The other
     four reuse info / warn / danger, exactly as the app's hex table does. */
  --cat-events: #8B5CF6; --cat-events-bg: #F5F3FF;
  --cat-policy: #059669; --cat-policy-bg: #ECFDF5;
}

/* ============================================================================
   COMPONENTS — this set's own classes. Adding one means declaring it here in
   the same edit: a class that resolves to nothing throws nothing and paints
   nothing.
   ============================================================================ */

/* screenshots.html sizes its tiles from the frame at runtime and publishes
   these three on the root. Declared here with the phone defaults it falls back
   to, so a token check can see them; the page's own values override. */
:root { --tile-w: 195px; --tile-h: 422px; --tile-scale: 0.5; }

.lucide { stroke-width: 2; }
.no-underline { text-decoration: none; }

/* The app mark both apps draw: a lucide shield on a blue -> indigo tile
   (AuthHeader.tsx, admin auth/login/page.tsx). */
.mark {
  display: grid; place-items: center;
  width: 64px; height: 64px; border-radius: var(--radius-lg);
  background: linear-gradient(to right, var(--shell-from), var(--shell));
  color: var(--ink-inverse);
}
.mark--sm { width: 48px; height: 48px; border-radius: var(--radius); }

/* Buttons. `btn-ink` is the mobile app's primary (bg-gray-900); `btn-info` is
   ui/Button's default variant (#3B82F6); the rest are its named variants. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 12px 16px; min-height: 44px;
  border-radius: var(--radius); border: 1px solid transparent;
  font-size: 14px; font-weight: 600; line-height: 1.2;
  text-decoration: none; cursor: pointer;
}
.btn-ink     { background: var(--ink);     color: var(--ink-inverse); font-weight: 400; }
.btn-info    { background: var(--info);    color: var(--ink-inverse); }
.btn-success { background: var(--success); color: var(--ink-inverse); }
.btn-warn    { background: var(--warn);    color: var(--ink-inverse); }
.btn-danger  { background: var(--danger);  color: var(--ink-inverse); }
.btn-quiet   { background: var(--sunken);  color: var(--ink-2); border-color: var(--line); }
.btn-ghost   { background: transparent;    color: var(--ink-2); border-color: var(--line-strong); }
.btn-off     { background: var(--line-strong); color: var(--ink-2); font-weight: 400; }
.btn-sm      { padding: 8px 12px; min-height: 36px; font-size: 12px; }
.btn-block   { width: 100%; }

/* Cards. ui/Card and ui/SectionCard: white, rounded-2xl, gray-200 border. */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 16px;
}
.card--shadow { box-shadow: var(--shadow-sm); }
.card--info   { background: var(--info-bg);   border-color: var(--info-bg); }
.card--warn   { background: var(--warn-bg);   border-color: var(--warn-bg); }
.card--danger { background: var(--danger-bg); border-color: var(--danger-bg); }

/* A text input drawn as the app draws it: an icon and a value in a bordered row. */
.field {
  display: flex; align-items: center; gap: 8px;
  padding: 12px; border: 1px solid var(--line-strong); border-radius: var(--radius);
  color: var(--ink-4); font-size: 14px; background: var(--surface);
}
.field--filled { color: var(--ink); }
.field--error  { border-color: var(--danger); }
.field-label { display: block; margin-bottom: 4px; font-size: 13px; color: var(--ink-3); }

/* Badges. ui/Badge: 100-level wash, 800-level text. */
.badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 4px 8px; border-radius: var(--radius-pill);
  font-size: 10px; font-weight: 600; line-height: 1.3; white-space: nowrap;
}
.badge--md      { padding: 6px 10px; font-size: 12px; }
.badge--success { background: var(--success-bg); color: var(--success-fg); }
.badge--warn    { background: var(--warn-bg);    color: var(--warn-fg); }
.badge--danger  { background: var(--danger-bg);  color: var(--danger-fg); }
.badge--info    { background: var(--info-bg);    color: var(--info-fg); }
.badge--neutral { background: var(--sunken);     color: var(--ink-2); }

/* A filter pill; `.is-on` is the selected one. */
.pill {
  display: inline-flex; align-items: center; padding: 8px 16px;
  border-radius: var(--radius-pill); background: var(--line);
  color: var(--ink-2); font-size: 13px; font-weight: 500; white-space: nowrap;
  text-decoration: none;
}
.pill.is-on { background: var(--info); color: var(--ink-inverse); }

/* ui/Skeleton: a gray-200 bar. */
.skel { display: block; height: 12px; border-radius: var(--radius-sm); background: var(--line); }

/* Modal furniture. The card over a backdrop is bg-frame, never bg-surface. */
.backdrop { position: absolute; inset: 0; background: rgb(0 0 0 / 0.4); }
.bottom-sheet {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: var(--frame); padding: 16px;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.dialog {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(448px, calc(100% - 48px));
  background: var(--frame); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
}

/* Patterns pages are frameless specimen sheets. */
.section-label {
  font-size: 10px; text-transform: uppercase; letter-spacing: var(--tracking-label);
  color: var(--ink-3); font-weight: 700;
}

/* ---- web only ------------------------------------------------------------- */

/* A website frame is not a device. CORE's .phone assumes a 40px device radius;
   only the rounding and the shadow need correcting. */
.phone {
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}
/* Bare mode is the real page: it fills whatever window it is given. CORE pins
   the frame's logical width, which is right for a device and wrong for a site.
   In the screenshots sheet the iframe is 1440 wide, so this still renders 1440. */
[data-bare] .phone { width: 100%; }

/* shared/Container.tsx: `container mx-auto px-4 sm:px-6 lg:px-8`. */
.wrap { width: 100%; max-width: 1280px; margin-inline: auto; padding-inline: 32px; }
.wrap--narrow { max-width: 768px; }
.wrap--mid    { max-width: 896px; }

/* Header.tsx — sticky, translucent, 56px. */
.site-head {
  position: sticky; top: 0; z-index: 5; flex: none;
  background: rgb(255 255 255 / 0.85); backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}
.nav-link { color: var(--ink-3); font-size: 14px; font-weight: 500; text-decoration: none; }
.nav-link:hover { color: var(--ink); }
.foot-link { color: var(--ink); font-size: 12px; text-decoration: none; }

/* RESERVED. The brand gradient: the logo wordmark and the primary sell. */
.btn-brand {
  background: linear-gradient(to right, var(--brand-600), var(--accent-500));
  color: var(--ink-inverse); border-radius: var(--radius-sm);
}
.grad-text {
  background: linear-gradient(to right, var(--brand-600), var(--accent-500));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.btn-outline {
  background: var(--surface); color: var(--ink); border-color: var(--line-strong);
  border-radius: var(--radius-sm); font-weight: 500;
}

/* The pill above every section heading (`bg-secondary inline-block rounded-lg`). */
.eyebrow {
  display: inline-block; padding: 4px 12px; border-radius: var(--radius-sm);
  background: var(--sunken); color: var(--ink); font-size: 14px;
}
.wcard {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 24px;
}
.input {
  display: flex; align-items: center; width: 100%; min-height: 40px;
  padding: 8px 12px; border: 1px solid var(--line-strong);
  border-radius: 6px; background: var(--surface);
  color: var(--ink-4); font-size: 14px;
}
.input--filled { color: var(--ink); }
.input--error  { border-color: var(--danger); }
.input--tall   { min-height: 160px; align-items: flex-start; }

/* ---- the admin console ---------------------------------------------------- */
.adm { display: flex; flex: 1; min-height: 0; background: var(--muted); }
.adm-side {
  display: flex; flex-direction: column; width: 208px; flex: none;
  background: var(--surface); border-right: 1px solid var(--line);
}
.adm-nav {
  display: flex; align-items: center; gap: 12px; padding: 10px 12px;
  border-radius: 6px; color: var(--ink-2); font-size: 14px; font-weight: 500;
  text-decoration: none;
}
.adm-nav.is-active { background: var(--shell); color: var(--ink-inverse); box-shadow: var(--shadow-sm); }
.adm-nav--dead { color: var(--ink-4); }
.adm-top {
  display: flex; align-items: center; gap: 16px; flex: none;
  padding: 16px 24px 8px; background: var(--surface); border-bottom: 1px solid var(--line);
}
.adm-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.adm-body { flex: 1; padding: 24px; }
.adm-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 32px; padding: 0 12px; border-radius: 6px; border: 1px solid transparent;
  font-size: 14px; font-weight: 500; text-decoration: none; white-space: nowrap;
}
.adm-btn--primary { background: var(--shell-from); color: var(--ink-inverse); }
.adm-btn--default { background: var(--shell); color: var(--ink-inverse); }
.adm-btn--outline { background: var(--surface); color: var(--ink-2); border-color: var(--line-strong); }
.adm-btn--ghost   { color: var(--ink-2); }
.adm-btn--danger  { background: var(--danger); color: var(--ink-inverse); }
.adm-btn--success { background: var(--success); color: var(--ink-inverse); }
.adm-btn--lg      { height: 40px; padding: 0 16px; }
.tbl { width: 100%; border-collapse: collapse; font-size: 14px; }
.tbl th {
  text-align: left; padding: 12px 16px; font-size: 12px; font-weight: 500;
  color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.05em;
  background: var(--muted); border-bottom: 1px solid var(--line);
}
.tbl td { padding: 14px 16px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.tile {
  display: grid; place-items: center; width: 40px; height: 40px; flex: none;
  border-radius: var(--radius-sm); background: var(--info-bg); color: var(--info);
}
.tile--sm { width: 32px; height: 32px; }
.tile--success { background: var(--success-bg); color: var(--success); }
.tile--warn    { background: var(--warn-bg);    color: var(--warn); }
.tile--danger  { background: var(--danger-bg);  color: var(--danger); }
.tile--neutral { background: var(--sunken);     color: var(--ink-3); }
