/*
 * design.css — FROZEN platform design system. The agent cannot write this file;
 * frozenSync re-copies it into every app on every build, so a fix here reaches
 * the whole v2 fleet on each app's next rebuild.
 *
 * THREE GENRES, ONE SYSTEM
 * The token layer below has three treatments of the same structure:
 *   (default)             app / dashboard — warm paper, cobalt accent
 *   <body data-genre="landing">  marketing page — same palette, editorial scale
 *   <body data-genre="game">     arcade — near-black, violet/mint/amber HUD
 * A genre is chosen by setting data-genre on <body> in views/layout.ts. Every
 * component below reads tokens, so switching genre restyles the whole app
 * without touching a single component rule.
 *
 * THE TOKEN CONTRACT
 * Every brand-able value is consumed as var(--token) and every token's DEFAULT
 * is defined here. public/theme.css is loaded AFTER this file and is agent-
 * writable, so an app brands itself by re-declaring only the tokens it needs.
 * Defaults live here, not there, on purpose: theme.css is writable, and a
 * broken or emptied theme must degrade to this look rather than leave the
 * system with undefined values and an unstyled app.
 *
 * CSP: generated apps run under script-src 'self' and default-src 'self'.
 * No @import, no external url(), no CDN, no data: URI fonts (data: is allowed
 * in img-src only). A webfont must be a self-hosted file under public/ that is
 * ALSO registered in the frozen src/lib/assets.ts allowlist, or it 404s. The
 * font stacks below name Public Sans and IBM Plex Mono first and fall back to
 * system faces, so they upgrade automatically once those files are hosted.
 */

/* ================================================================== *
 * FONTS — self-hosted, latin subset. Both are SIL OFL 1.1 (see
 * public/fonts/LICENSE.txt). They must be served from our own origin:
 * default-src 'self' blocks every font CDN, and a data: URI would be blocked
 * too (data: is allowed in img-src only). Each file also needs an entry in
 * the frozen src/lib/assets.ts allowlist or it 404s and the stack silently
 * falls back to system faces.
 * ================================================================== */

@font-face {
  font-family: "Public Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/public-sans-400.woff2") format("woff2");
}

@font-face {
  font-family: "Public Sans";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/static/fonts/public-sans-500.woff2") format("woff2");
}

@font-face {
  font-family: "Public Sans";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/public-sans-600.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-mono-400.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-mono-500.woff2") format("woff2");
}

/* ================================================================== *
 * TOKENS
 * ================================================================== */

:root {
  /* surfaces */
  --canvas: #f7f6f3;
  --surface: #fbfaf8;
  --card: #ffffff;
  --border: #e7e3db;
  --border-strong: #d8d3c8;

  /* ink */
  --ink: #16151a;
  --muted: #6f6b62;
  --icon: #8a8579;

  /* accent + status */
  --accent: #2b44e0;
  --accent-soft: #eef0fe;
  --accent-ink: #ffffff;
  --success: #17845a;
  --success-soft: #e7f4ee;
  --warning: #b45309;
  --warning-soft: #fdf0e3;
  --danger: #b42318;
  --danger-soft: #fdeceb;

  /* type */
  --font-sans: "Public Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* shape — 6 small controls, 8 inputs/nav, 12 cards */
  --radius-sm: 6px;
  --radius: 8px;
  --radius-lg: 12px;

  /* Elevation stays HAIRLINE by rule — the flat card shadow is the spec'd one
     and must never get heavier. Depth comes from a second, wider and softer
     ambient layer at very low alpha, not from a darker drop: that is the
     difference between "raised" and "cheap". --shadow-raised is for a surface
     that is deliberately lifted (hover, popover), never the resting card. */
  --shadow: 0 1px 2px rgba(22, 21, 26, 0.04);
  --shadow-raised:
    0 1px 2px rgba(22, 21, 26, 0.05),
    0 8px 24px -8px rgba(22, 21, 26, 0.08);

  /* Interaction. A focus ring that is the accent at low alpha reads as part of
     the system rather than an OS artefact; the tint is the hover surface, so a
     hover never has to invent a colour. */
  --ring: 0 0 0 3px var(--accent-soft);
  --tint: color-mix(in srgb, var(--ink) 3%, transparent);

  /* Motion. One duration and one easing for the whole system — inconsistent
     timing is the most common reason an otherwise clean UI feels cheap. The
     curve settles rather than bounces. */
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
  --dur: 150ms;

  /* Spacing rhythm — a 4px scale, so padding across components is related
     rather than hand-picked per rule. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  /* Optical tracking. Type tightens as it grows: at display sizes default
     spacing reads loose and amateur, at small sizes it needs to stay open. */
  --tracking-display: -0.038em;
  --tracking-head: -0.03em;
  --tracking-tight: -0.02em;
  --tracking-caps: 0.06em;

  /* layout */
  --sidebar-w: 244px;
  --row-h: 46px;
  /* The mobile nav bar. ONE token, read by BOTH states: the no-JS strip and the
     bar the drawer leaves behind. That is what makes them the same height by
     construction rather than by coincidence — if they differ by even 2px, the
     upgrade from one to the other is a visible jump. */
  --navbar-h: 56px;
  --content-max: 1180px;
}

/* Landing: same palette, editorial scale, alternating section bands. */
body[data-genre="landing"] {
  --canvas: #f7f6f3;
  --surface: #fbfaf8;
  --content-max: 1120px;
}

/* Game: inverted shell, mono HUD. */
body[data-genre="game"] {
  --canvas: #0d0c11;
  --surface: #131118;
  --card: #1b1922;
  --border: #2f2c39;
  --border-strong: #3b3747;
  --ink: #f4f2f7;
  --muted: #7b7688;
  --icon: #7b7688;
  --accent: #7c5cff;
  --accent-soft: #1f1b33;
  --accent-ink: #ffffff;
  --success: #7cf7c1;
  --success-soft: #14261f;
  --warning: #ffd166;
  --warning-soft: #2a2318;
  --shadow: none;
}

/* ================================================================== *
 * BASE
 * ================================================================== */

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  /* An id, a filename, an email, a hash and a bare URL are all one word with no
     space in it, and a generated app fills up with them. Without this a single
     such string cannot break and pushes its whole container past the viewport —
     ten of the thirteen shapes the layout check found were this and nothing else.
     Inherited, so it reaches every element the generator emits.

     `anywhere` and not `break-word`: break-word breaks the LINE but leaves the
     element's min-content contribution as the longest word, so a flex or grid
     parent is still forced open by it. Measured, that difference is four of the
     ten — .cell, .hero, .hud and a .list row all still overflowed on break-word
     and are clean on anywhere.

     No word-break and no hyphens with it. word-break: break-all breaks every
     word whether it needs it or not, which is a wrapping style rather than an
     overflow fix; hyphens is cosmetic, needs a lang attribute, and changes no
     width. */
  overflow-wrap: anywhere;
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.5;
  background: var(--canvas);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

/* Replaced media renders at its INTRINSIC size, which has nothing to do with the
   screen: an 1200px-wide image took the page 871px past a 360px viewport. There
   was no img rule in this file at all — the only max-width:100% in it belonged
   to .tablewrap.
   height:auto only where a ratio has to be preserved from a width attribute.
   NOT on svg: inline icons carry explicit width and height, and letting one of
   them compute its own height collapses it. */
img,
video,
canvas,
svg,
iframe,
embed,
object {
  max-width: 100%;
}

img,
video {
  height: auto;
}

/* <pre> had no rule either, and it is the one text element overflow-wrap cannot
   reach: white-space:pre forbids the line break that overflow-wrap would take.
   So the block scrolls instead — the same treatment .tablewrap gets, and the
   right one for code, where wrapping a line misrepresents it. */
pre {
  max-width: 100%;
  overflow-x: auto;
}

/* A bare <a> is usually STRUCTURAL — nav, a card that links, a brand mark —
   and those must not read as body-copy links. Left at the browser default they
   came out underlined and, once tinted, painted every nav item accent-blue,
   which breaks the one-accent-per-page rule (the accent belongs to the primary
   CTA alone). So the base link inherits its surroundings and only reveals
   itself on hover; a link inside PROSE opts back into looking like a link.
   Anything carrying .btn keeps its own colour — a class beats an element. */
a {
  color: inherit;
  text-decoration: none;
}

/* :not(.btn) on both of these, and it is load-bearing (2026-08-13).
   An element selector with a pseudo-class or an ancestor outranks a bare class:
   `a:hover` and `.empty a` are both (0,1,1), `.btn` is (0,1,0). So an anchor
   rendered as a button LOST to them — every `<a class="btn">` inside an empty
   state came out underlined and accent-coloured instead of a solid button, and
   every link-button anywhere recoloured its label on hover. Source order does
   not save it: `.btn` is four hundred lines further down and still loses.
   Reported from a real generated app, whose agent could only work around it in
   its own markup — design.css is frozen, so the app side cannot reach this.
   These three selectors mean "a link in PROSE". A button is not prose, so the
   right fix is for them not to MATCH it, rather than for `.btn` to fight back
   with a heavier selector. `.btn` re-states its own colour at :hover as well —
   see the note there for why both halves are kept. */
a:not(.btn):hover {
  color: var(--accent);
}

p a:not(.btn),
.empty a:not(.btn) {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}

/* Every number, ID, badge and metric is mono — this is what stops a data app
   from reading like a document. */
.num,
td.num,
th.num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* ================================================================== *
 * APP SHELL — sidebar + topbar + content
 * ================================================================== */

.shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  min-height: 100vh;
}

/* A grid item's default min-width is AUTO, not 0 — it refuses to shrink below
   its content's min-content width. So one wide table drags the whole 1fr track
   past the viewport and the PAGE scrolls sideways: measured 192px of horizontal
   overflow at 375px, with the shell's single column computing to 566.672px in a
   375px window.
   The second cost is quieter and worse. .tablewrap already carries
   overflow-x:auto so a wide table scrolls INSIDE its card — but it never
   received a narrow box to scroll in, so it never scrolled once. This one
   declaration both stops the page overflowing and lets that existing rule do
   the job it was written for. */
.shell > * {
  min-width: 0;
}

.sidebar {
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 14px 12px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.sidebar .brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-weight: 600;
  padding: 4px 8px 0;
}

.sidebar .brand .mark {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
  font-size: 13px;
}

.navgroup {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.navgroup > .label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--icon);
  padding: 6px 8px;
}

.navgroup a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 8px;
  border-radius: var(--radius);
  color: var(--ink);
  text-decoration: none;
  font-weight: 500;
  box-shadow: inset 0 0 0 1px transparent;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

.navgroup a:hover {
  background: var(--tint);
}

.navgroup a[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent);
}

.navgroup a .count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--muted);
}

.sidebar .foot {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.topbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 22px;
  background: var(--card);
  border-bottom: 1px solid var(--border);
}

.topbar .spacer {
  flex: 1;
}

.crumbs {
  color: var(--muted);
}

.crumbs b {
  color: var(--ink);
  font-weight: 500;
}

/* The cap without the centering left the column pinned to the sidebar and every
   pixel past it dead: on a 1920 screen the content ended at 1424 and 465px of
   canvas sat unused to the right. The landing genre already centers its own
   container (see the .band rule); this is the app genre doing the same thing. */
.content {
  padding: 22px;
  max-width: var(--content-max);
  margin-inline: auto;
}

.page-head {
  margin-bottom: 18px;
}

/* ================================================================== *
 * TYPE
 * ================================================================== */

/* A heading needs its own ratio. Left undeclared these inherited the body's
   1.5, which is a BODY-COPY ratio: it put 11px of leading inside a 22px title
   and made every two-line heading read as two separate lines. The ratio falls
   as the size rises, which is why h1 is tighter than h2. */
h1 {
  font-size: 22px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: var(--tracking-tight);
  margin: 0 0 4px;
}

h2 {
  font-size: 16px;
  font-weight: 600;
  line-height: 1.25;
  margin: 0 0 4px;
}

/* Levels 3 to 6 had NO rule at all, so they fell through to the browser's own
   em-relative sizes against a 14px body — and the scale came out inverted:
   h3 rendered at 16.38px/700, both LARGER and HEAVIER than the h2 above it, and
   every level below carried a 16-22px margin nothing in this system asked for.
   An app that used <h3> for a subsection got a heading that outranked its own
   section title, which is the kind of wrongness that reads as "the pages look
   inconsistent" without anyone being able to point at it.
   One weight for the whole scale, sizes that only ever go down, and the same
   `0 0 4px` h1 and h2 already use. */
h3 {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.35;
  margin: 0 0 4px;
}

h4,
h5,
h6 {
  font-size: 13px;
  font-weight: 600;
  line-height: 1.35;
  margin: 0 0 4px;
}

/* Prose. Also unstyled until now: a <p> carried the browser's 1em top AND
   bottom margin, so two paragraphs sat 14px apart in a card whose own rhythm is
   16px, and a list was indented 40px — outside the grid everything else lines
   up to. The trailing margin is dropped on the last child so a card does not
   end in a gap. */
/* A paragraph gets its rhythm on BOTH sides, not just below (2026-08-13).
   `margin: 0 0 12px` works for as long as a paragraph follows a paragraph —
   the two margins collapse and 12px is what you get. It fails the moment one
   follows something that has no bottom margin of its own: an input, a control
   row, a table. That is exactly the helper line the reference example teaches
   (`<p class="muted">No notes yet.</p>`), which therefore sat flush against the
   bottom edge of the field above it in a real app.
   Adjacent margins still collapse, so paragraph-after-paragraph is unchanged at
   12px. The one case that would regress is a paragraph FIRST inside a padded
   box, where padding blocks the collapse and the 12px would be added on top —
   so `:first-child` takes it back. Every deliberate exception below
   (`.stack > p`, `.empty p`, `.hero p`) already states its own margin and is
   unaffected. */
p {
  margin: 12px 0;
}

p:first-child {
  margin-top: 0;
}

ul,
ol {
  margin: 0 0 12px;
  padding-left: 20px;
}

blockquote {
  margin: 0 0 12px;
  padding-left: 12px;
  border-left: 2px solid var(--border);
  color: var(--muted);
}

figure {
  margin: 0 0 12px;
}

p:last-child,
ul:last-child,
ol:last-child,
blockquote:last-child,
figure:last-child {
  margin-bottom: 0;
}

/* The browser's <hr> is a 2px inset ridge — a 3D bevel from 1996 sitting in a
   flat system. It is the same hairline every card and table row uses. */
hr {
  height: 0;
  border: 0;
  border-top: 1px solid var(--border);
  margin: 16px 0;
}

.muted {
  color: var(--muted);
}

.small {
  font-size: 12px;
}

/* ================================================================== *
 * CARD
 * ================================================================== */

.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  /* How much padding THIS card has, for the .list pull-back below to cancel.
     Raised to 16px only on a card that actually gets padding. Declared here on
     every .card because custom properties INHERIT: without the reset a card
     nested inside a padded one would inherit 16px and pull its list out on a
     padding it does not have. */
  --card-pad: 0px;
}

.card-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--border);
}

.card-head h2 {
  margin: 0;
}

.card-head .spacer {
  flex: 1;
}

.card-body {
  padding: 16px;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* A .stack OWNS the rhythm between its children, so a child that also carries a
   block margin adds to it — flex does not collapse margins, so the two stack up.
   Measured on a real form: a .field (14px bottom margin) followed by a hint <p>
   (12px top margin, UA default) inside a 16px stack rendered 42px apart, three
   times the 14px the same form used between its own fields.
   Scoped to DIRECT children on purpose: .field keeps its margin everywhere else,
   which is what spaces fields inside a plain .card-body. */
.stack > .field {
  margin-bottom: 0;
}

.stack > p {
  margin-block: 0;
}

.grid {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}

/* SELF-HEALING LAYOUT.
   A .card is a SURFACE: its padding lives on .card-head / .card-body, and the
   space between two cards comes from .stack. Both are opt-in, so markup that
   skips them renders content flush to the border with no gap between cards —
   valid CSS, no error anywhere, wrong layout. These two rules repair that case
   without touching markup that composed correctly.

   The padding rule is written as :not(:has(...)) deliberately. Stated the other
   way round — pad every card, then zero the ones that carry a body — a browser
   without :has() drops the RESET and doubles the inset on correct markup. This
   way an unsupported :has() invalidates the whole selector, the rule is dropped
   and the card falls back to exactly the behaviour it has today. The fallback
   is the bug, not a worse bug.

   A card holding a table is excluded too: a table runs edge to edge on purpose
   so its header fill and row rules can span the full card width. A .list is
   NOT excluded — see the pull-back rule below for why. */

/* ORDER DEPENDENCY — THIS RULE MUST STAY ABOVE .steps .card.
   Both compute to (0,2,0): .card is one class, and :not() takes the
   specificity of its most specific argument, which is the :has(), which in
   turn takes its own most specific argument — a class either way. .steps .card
   is two classes. Equal specificity means SOURCE ORDER decides it, so the
   landing genre keeps its 18px only by being declared later in this file.
   Move either rule past the other and .steps silently drops to 16px, with
   nothing failing. designSystem.test.ts resolves this exact pair and asserts
   the winner is 18px, so a reorder breaks the suite instead of the layout. */
.card:not(:has(> .card-head, > .card-body, > .empty, > table, > .tablewrap)) {
  padding: 16px;
  --card-pad: 16px;
}

/* A .list sits INSIDE that padding and is pulled back out to the card's edges,
   rather than being excluded from it.

   Excluding it was the obvious move and it was wrong: whether a card holds a
   .list depends on the DATA, not on the markup. The reference shape renders a
   <p> when the collection is empty and a .list once it has rows, so the card
   was padded while empty and unpadded the moment the first row landed — the
   heading jumped to the border on the first insert. Padding that changes with
   the contents of the database is harder to diagnose than padding that is
   simply always wrong.

   Pulled per EDGE, never as a blanket margin:-16px. An all-sides negative
   margin drags the list UP over whatever heading sits above it, since h1/h2
   only carry a 4px bottom margin. Horizontal always; top only when the list
   opens the card; bottom only when it closes it. A card that is nothing but a
   list therefore nets back to zero and renders exactly as it did before.

   The row keeps its own 10px 16px inset (see .list > li), so the row TEXT now
   lands on the same left edge as the heading while the divider still spans the
   full card width — the two were 16px apart before. .card-body > .list is a
   different shape and is deliberately untouched here.

   The pull is bound to --card-pad, not written as a literal, because it must
   cancel a padding that is CONDITIONAL. Written as a flat -16px it also fired
   on cards excluded from the padding rule — an "At a glance" card with a
   .card-head has no padding to cancel, so its list was dragged 16px clean
   outside the card on both sides and the row text landed 16px LEFT of the
   card-head's own heading. Nothing errored; it read as a card whose rows were
   mysteriously wider than its header. */
.card > .list {
  margin-inline: calc(-1 * var(--card-pad));
}

.card > .list:first-child {
  margin-top: calc(-1 * var(--card-pad));
}

.card > .list:last-child {
  margin-bottom: calc(-1 * var(--card-pad));
}

/* Excluded wherever the parent ALREADY provides the gap: a margin on a flex or
   grid child ADDS to that container's gap, so an unscoped rule would open
   .stack to 32px instead of 16px. */
:not(.stack, .grid, .hero, .steps, .row, .actions) > .card + .card {
  margin-top: 16px;
}

/* Stat tile — label, mono figure, delta chip. */
.stat {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: 14px 16px;
}

.stat .label {
  color: var(--muted);
  font-size: 12px;
  margin-bottom: 6px;
}

.stat .figure {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 24px;
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  display: flex;
  align-items: baseline;
  gap: 8px;
}

/* ================================================================== *
 * BUTTONS — primary is solid ink, never a coloured gradient
 * ================================================================== */

button,
.btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--ink);
  color: var(--card);
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  line-height: 1.3;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}

button,
.btn {
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}

/* Fading a button to 90% opacity fades it into the PAGE — the label goes grey
   with it. Mixing the surface toward ink keeps the text at full strength. */
button:hover,
/* text-decoration is RE-STATED here, not only excluded above: the exclusion
   says "a button is not a prose link", this says "a button knows what it looks
   like", so the next broad `a:hover` rule somebody adds cannot put a line
   through a label again. No variant sets text-decoration, so nothing can
   conflict with it.
   `color` is deliberately NOT re-stated. `.btn:hover` is (0,2,0) and so are
   `.btn.ghost` / `.accent` / `.danger` / `.success`, each of which sets its own
   colour and relies on it surviving the hover — a colour here would tie with
   them and be decided by source order alone, which is a trap waiting for the
   day somebody moves a block. The colour problem is already solved one rule
   up, by the hover no longer matching a button at all. */
.btn:hover {
  background: color-mix(in srgb, var(--ink) 88%, #fff);
  text-decoration: none;
}

button:active,
.btn:active {
  transform: translateY(0.5px);
}

button:focus-visible,
.btn:focus-visible,
a:focus-visible {
  outline: none;
  box-shadow: var(--ring);
}

button.ghost,
.btn.ghost {
  background: var(--card);
  color: var(--ink);
  border-color: var(--border);
}

button.ghost:hover,
.btn.ghost:hover {
  background: var(--tint);
  border-color: var(--border-strong);
}

button.accent,
.btn.accent {
  background: var(--accent);
  color: var(--accent-ink);
}

button.accent:hover,
.btn.accent:hover {
  background: color-mix(in srgb, var(--accent) 88%, #000);
}

/* A COLOUR class names the role; .solid fills it.
   .danger alone is the QUIET destructive — a Delete in a table row, where a
   filled red on every line shouts. .danger.solid is the loud one: the confirm
   button in a dialog, where the action is the point of the screen. The same
   pair exists for success, so "did it" and "will destroy it" read alike.
   .danger had no hover at all until now, while .ghost and .accent both did. */
button.danger,
.btn.danger {
  background: var(--card);
  color: var(--danger);
  border-color: var(--border);
}

button.danger:hover,
.btn.danger:hover {
  background: var(--danger-soft);
  border-color: color-mix(in srgb, var(--danger) 30%, transparent);
}

button.success,
.btn.success {
  background: var(--card);
  color: var(--success);
  border-color: var(--border);
}

button.success:hover,
.btn.success:hover {
  background: var(--success-soft);
  border-color: color-mix(in srgb, var(--success) 30%, transparent);
}

button.danger.solid,
.btn.danger.solid {
  background: var(--danger);
  color: #fff;
  border-color: transparent;
}

button.danger.solid:hover,
.btn.danger.solid:hover {
  background: color-mix(in srgb, var(--danger) 88%, #000);
}

button.success.solid,
.btn.success.solid {
  background: var(--success);
  color: #fff;
  border-color: transparent;
}

button.success.solid:hover,
.btn.success.solid:hover {
  background: color-mix(in srgb, var(--success) 88%, #000);
}

button.sm,
.btn.sm {
  padding: 4px 9px;
  font-size: 12px;
}

/* A FILLED button cannot be disabled with opacity: the label fades with the
   fill, so white-on-blue becomes pale-on-pale and the one state a button spends
   its time in — "you cannot press this yet" — is the least readable thing on
   screen. A filled control STATES its disabled colours; only an outlined one
   can be dimmed, and even then not below 0.6. */
button:disabled,
.btn:disabled {
  cursor: default;
  opacity: 0.6;
}

button:disabled,
.btn:disabled,
button.accent:disabled,
.btn.accent:disabled,
button.danger.solid:disabled,
.btn.danger.solid:disabled,
button.success.solid:disabled,
.btn.success.solid:disabled {
  opacity: 1;
  background: var(--border);
  color: var(--muted);
  border-color: transparent;
}

button.ghost:disabled,
.btn.ghost:disabled,
button.danger:disabled,
.btn.danger:disabled,
button.success:disabled,
.btn.success:disabled {
  opacity: 0.6;
  background: var(--card);
  border-color: var(--border);
}

/* An <input type="submit"> is a BUTTON, and it was matching the text-field rule
   instead: measured 35px tall with an 8px radius and a white fill, next to a
   32.9px, 6px-radius, ink-filled <button>. A form built with one came out
   looking like it had a stray empty field where its submit should be. */
/* And a checkbox or radio is a CONTROL, not a field — the same trap one family
   over, found in a real app on 2026-08-13. The text-field rule below sets
   `width:100%` plus padding, a border, a radius and a fill, so a checkbox
   stretched the width of its grid column and floated in the middle of it with
   a box drawn around the tick. It never lined up with the fields beside it
   because it was not being drawn as a checkbox at all.
   `input[type=…]` is (0,1,1) against the bare `input`'s (0,0,1), so this wins
   wherever it sits in the file — the same reason the submit reset above works.
   `accent-color` is what tints the NATIVE control: no custom box, no pseudo-
   element, and it follows the app's own --accent through every genre.
   Worth knowing why this one mattered more than it looks: design.css is frozen,
   so the agent that hit it could not fix the rule. It replaced the checkbox
   with a DROPDOWN — a boolean turned into a select to dodge a stylesheet bug.
   A broken platform rule does not just look wrong, it changes what gets built. */
input[type="checkbox"],
input[type="radio"] {
  width: auto;
  height: auto;
  flex: none;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  accent-color: var(--accent);
  cursor: pointer;
}

input[type="submit"],
input[type="button"],
input[type="reset"] {
  width: auto;
  padding: 7px 13px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--ink);
  color: var(--card);
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  line-height: 1.3;
  cursor: pointer;
}

.actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Segmented control (30 days / Quarter / Year). */
.segmented {
  display: inline-flex;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 2px;
  gap: 2px;
}

.segmented a,
.segmented button {
  background: transparent;
  color: var(--muted);
  border: 0;
  padding: 5px 11px;
  border-radius: var(--radius-sm);
  font-size: 12px;
  font-weight: 500;
  text-decoration: none;
}

.segmented [aria-current="true"] {
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow);
}

/* ================================================================== *
 * FORMS
 * ================================================================== */

/* Works both ways round: <label class="field"><span>Name</span><input></label>
   (no id/for needed) and <div class="field"><label>Name</label><input></div>. */
.field {
  display: block;
  margin-bottom: 14px;
}

.field > span,
.field > label {
  display: block;
  font-size: 12px;
  font-weight: 500;
  color: var(--muted);
  margin-bottom: 5px;
}

/* The HEIGHT is stated, and that is the whole point of it (2026-08-13).
   Without it a control's height is padding + border + a content box the browser
   sizes PER INPUT TYPE — and `type="date"` and `type="time"` lay out an
   internal editor plus a native picker button, so they came out taller than the
   text fields beside them and a form row stopped lining up. Reported from a
   real app, where the Date and Time fields sat visibly below their neighbours.
   36px is what the text fields already measured (8+8 padding + ~17 of 13px text
   + 2 border), so this changes nothing about how they look — it drags the
   picker types and the select back to the same line.
   The builder's own stylesheet solved this years-equivalent ago with
   `.ui-input { height:38px }`; this rule is that lesson, one system over. */
input,
textarea,
select {
  width: 100%;
  height: 36px;
  padding: 8px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font-family: inherit;
  font-size: 13px;
}

/* …and every control that is NOT a single-line field opts back out. A textarea
   is sized by its rows, and the button-shaped inputs carry their own padding.
   `height:auto` rather than omitting them from the rule above: the generic
   selector is what gives them their border, radius and font, and splitting that
   into two lists is how one of them silently stops matching later.
   (checkbox and radio already opt out in their own reset further up, which is
   (0,1,1) and wins wherever it sits.) */
textarea,
input[type="submit"],
input[type="button"],
input[type="reset"] {
  height: auto;
}

/* The native select arrow is drawn by the browser against the BORDER, and it
   ignores the padding every other control's icon respects — so next to a date
   field, whose calendar sits 11px in, the chevron reads as stuck to the edge.
   Drawn here instead, at the same 11px, so every icon in a filter row lines up.

   Two linear-gradients rather than an svg: the CSP these apps run under allows
   data: in img-src only, and designSystem.test.ts rejects any url(data:…) in
   this file outright. A file under /static/ would work but needs an assets.ts
   entry and a frozen path for one chevron. Gradients need neither.

   background-color is stated separately: naming `background` here would drop
   the image the shorthand does not carry. */
select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 30px;
  background-color: var(--card);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--icon) 50%),
    linear-gradient(135deg, var(--icon) 50%, transparent 50%);
  background-position: right 15px top 52%, right 11px top 52%;
  background-size: 4px 4px, 4px 4px;
  background-repeat: no-repeat;
}

input::placeholder,
textarea::placeholder {
  color: var(--icon);
}

input,
textarea,
select {
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

input:hover,
textarea:hover,
select:hover {
  border-color: var(--border-strong);
}

input:focus,
textarea:focus,
select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--ring);
}

.search {
  position: relative;
  min-width: 260px;
}

.search input {
  border-radius: var(--radius);
  background: var(--surface);
}

/* ================================================================== *
 * TABLE — 46px rows, mono IDs and amounts
 * ================================================================== */

.tablewrap {
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

th {
  text-align: left;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--muted);
  padding: 9px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  white-space: nowrap;
}

td {
  height: var(--row-h);
  padding: 0 16px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

tbody tr:last-child td {
  border-bottom: 0;
}

tbody tr {
  transition: background var(--dur) var(--ease);
}

tbody tr:hover td {
  background: var(--tint);
}

th.right,
td.right {
  text-align: right;
}

/* Avatar chip used in identity cells. */
.avatar {
  display: inline-grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
}

.cell {
  display: flex;
  align-items: center;
  gap: 9px;
}

/* Plain list — the non-tabular row stack (run logs, simple item lists). */
.list {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* The inset lives on the ROW, not on the list box, for two reasons. A list is
   usually dropped straight into a .card, and `.list { padding: 0 }` (which kills
   the browser's 40px ul indent) is declared after .card-body — equal specificity,
   later wins — so `class="list card-body"` silently lost ALL its padding and the
   rows ran to the card edges while the header stayed inset (2026-08-08). Putting
   it here makes both `list` and `list card-body` render identically, with no
   doubled padding. It also lets the divider span the full card width while the
   content stays aligned, which is what the reference tables do. */
/* Height is the LIST's own, not --row-h. That token is the TABLE row (see the
   td rule), and a table row carries a full cell of content; a list row is a
   label and a trailing badge, so 46px left 21px of text sitting in 25px of
   air. 40px, with the padding brought down to match.
   Note where this stops paying: .badge is 24.5px tall, so on a row that ends
   in one the badge sets the floor at 42.5px and min-height no longer binds.
   Going tighter than this only separates badge rows from text rows further. */
.list > li {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  padding: 9px 16px;
  border-top: 1px solid var(--border);
}

.list > li:first-child {
  border-top: 0;
}

/* ================================================================== *
 * BADGE — mono, dot + label
 * ================================================================== */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
}

.badge::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
}

.badge.plain::before {
  display: none;
}

.badge.success {
  background: var(--success-soft);
  border-color: transparent;
  color: var(--success);
}

.badge.warning {
  background: var(--warning-soft);
  border-color: transparent;
  color: var(--warning);
}

.badge.danger {
  background: var(--danger-soft);
  border-color: transparent;
  color: var(--danger);
}

.badge.accent {
  background: var(--accent-soft);
  border-color: transparent;
  color: var(--accent);
}

/* ================================================================== *
 * EMPTY STATE — graphic + one-line reason + primary action.
 * Never a bare zero and never a blank table.
 * ================================================================== */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 10px;
  padding: 46px 24px;
}

.empty .graphic {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border-radius: var(--radius-lg);
  background: var(--accent-soft);
  color: var(--accent);
}

.empty h2 {
  margin: 0;
}

.empty p {
  margin: 0;
  color: var(--muted);
  max-width: 42ch;
}

/* ================================================================== *
 * LANDING
 * ================================================================== */

body[data-genre="landing"] .band {
  background: var(--canvas);
  padding: 72px 22px;
}

body[data-genre="landing"] .band:nth-child(even) {
  background: var(--surface);
}

/* A HEADER is a bar, not a section. .band is the section primitive and its
   72px is section breathing room; used for a header it puts 144px of padding
   around a 33px bar, which is 81% air. The generator reaches for .band because
   there is no header primitive to reach for — the same substitution that made
   it reach for .actions, a button gap, to space a nav.

   The selector has to name .band. An element-only hook is (0,1,2) against
   .band's (0,2,1) and LOSES on class count whatever the source order — checked
   in a browser, not deduced. So this corrects the one property that is wrong
   and adds no layout: the bar's own row already carries that, and the nav rule
   below already spaces the links. 12px matches .topbar, the app genre's bar. */
body[data-genre="landing"] header.band {
  padding: 12px 22px;
}

body[data-genre="landing"] .band > .inner {
  max-width: var(--content-max);
  margin: 0 auto;
}

/* HEADER NAV. There is no primitive for it, so the generator reaches for
   .actions — whose 8px gap is a BUTTON gap. Four text links 8px apart read as
   one run of words rather than as four targets.

   Hooked on the <nav> ELEMENT rather than on a class, for two reasons: the
   class is whatever the generator happened to pick, and an element hook still
   outranks .actions on specificity — (0,1,2) against (0,1,0) — so it corrects
   that choice instead of colliding with it.

   MARKUP CONTRACT, AND IT IS UNVERIFIED BEYOND ONE APP: a landing header nav
   is a <nav> in the landing genre holding its links as direct children. That
   shape was read off a SINGLE local landing app, which is the only one on this
   machine; the deployed fleet was not inspected. The scoping is what makes
   that acceptable — a landing app that structures its nav differently simply
   does not match, and nothing it renders today changes. */
body[data-genre="landing"] nav {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 28px;
}

.hero {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 54px;
  align-items: center;
}

/* The size here is FLUID and the ratio was FIXED, which cannot be right at both
   ends: 1.04 is a display ratio for 64px, and at the clamp's 40px floor it left
   1.6px of leading. Inside line-height, em resolves against the element's OWN
   font-size — so one declaration tracks the clamp by itself, giving a looser
   ratio to the small end and a tighter one to the large end. It also cannot
   drift: a parallel vw formula would have to be retuned by hand every time the
   font clamp above changes, and nothing would report it if that were missed. */
.hero h1 {
  font-size: clamp(40px, 5vw, 64px);
  font-weight: 600;
  line-height: calc(1em + 6px);
  letter-spacing: var(--tracking-display);
  margin: 0 0 18px;
}

/* The one coloured phrase in a headline. */
.hero h1 em,
.display em {
  font-style: normal;
  color: var(--accent);
}

.hero p {
  font-size: 16px;
  color: var(--muted);
  max-width: 46ch;
  margin: 0 0 24px;
}

.display {
  font-size: clamp(26px, 3vw, 34px);
  font-weight: 600;
  letter-spacing: var(--tracking-head);
  line-height: 1.15;
  margin: 0 0 10px;
}

.eyebrow {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 10px;
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 12px;
  border-radius: 999px;
  background: var(--card);
  border: 1px solid var(--border);
  font-size: 12px;
  margin-bottom: 22px;
}

/* Exactly one dark card per landing page, for contrast. */
.card.invert {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--card);
}

.card.invert .muted,
.card.invert p {
  color: color-mix(in srgb, var(--card) 68%, transparent);
}

.steps {
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}

.steps .card {
  padding: 18px;
}

.steps .n {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--muted);
  margin-bottom: 10px;
}

/* ================================================================== *
 * GAME
 * ================================================================== */

body[data-genre="game"] .hud {
  display: flex;
  align-items: stretch;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  font-family: var(--font-mono);
}

body[data-genre="game"] .hud .slot {
  padding: 9px 18px;
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 3px;
}

body[data-genre="game"] .hud .slot .label {
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

body[data-genre="game"] .hud .slot .value {
  font-size: 19px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

body[data-genre="game"] .hud .spacer {
  flex: 1;
  border-right: 0;
}

/* Radial glow + masked grid, never flat black. */
.playfield {
  position: relative;
  background:
    radial-gradient(ellipse at 50% 40%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 62%),
    linear-gradient(var(--border) 1px, transparent 1px) 0 0 / 40px 40px,
    linear-gradient(90deg, var(--border) 1px, transparent 1px) 0 0 / 40px 40px,
    var(--canvas);
  -webkit-mask-image: radial-gradient(ellipse at 50% 45%, #000 55%, transparent 100%);
  mask-image: radial-gradient(ellipse at 50% 45%, #000 55%, transparent 100%);
  min-height: 420px;
}

/* Entities glow in their own colour. */
.glow {
  filter: drop-shadow(0 0 6px currentColor);
}

.keycap {
  display: inline-grid;
  place-items: center;
  min-width: 22px;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  background: var(--card);
  border: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--muted);
}

/* Pause / game-over overlay. */
.overlay {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgba(9, 8, 12, 0.72);
}

.overlay .panel {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 26px;
  text-align: center;
  min-width: 300px;
}

/* ================================================================== *
 * UTILITIES
 * ================================================================== */

.row {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* .row is a CONTROL row — a brand, a spacer, some buttons — and centring is
   right for that. It is not a page layout, but there is no two-column layout
   primitive, so the generator reaches for it and gets two side-by-side cards
   CENTRED against each other: measured 15px of top-edge offset on a form,
   exactly half the height difference between them.
   `start` rather than `stretch`: stretch would also top-align, but it forces the
   shorter card to the taller one's height, and .card declares no internal
   layout, so the 30px it gains renders as dead space at the bottom. Natural
   heights, aligned tops. .row's own centring is untouched, so every control row
   in the fleet is unaffected. */
.row > .card {
  align-self: start;
}

.grow {
  flex: 1;
}

.right {
  text-align: right;
}

/* ================================================================== *
 * RESPONSIVE — the sidebar folds above the content on narrow screens.
 * ================================================================== */

@media (max-width: 860px) {
  .shell {
    grid-template-columns: 1fr;
    /* The rows have to be stated. .shell carries min-height:100vh, and a grid
       with implicit rows STRETCHES them to fill it — so a short first row is
       inflated to share the viewport with the second. With the sidebar in flow
       that was invisible, because it filled its own row. The moment the drawer
       took it out of flow and left a 56px bar behind, that bar's row grew to
       369px and opened 313px of blank page under it.
       auto for the bar, 1fr for everything else. Correct in BOTH states: the
       no-JS strip and the injected bar are each the only in-flow first child. */
    grid-template-rows: auto 1fr;
  }

  .sidebar {
    border-right: 0;
    border-bottom: 1px solid var(--border);
  }

  .sidebar .foot {
    margin-top: 0;
  }

  .hero {
    grid-template-columns: 1fr;
    gap: 30px;
  }

  /* MOBILE NAV, FLOOR STATE — no JavaScript involved.
     The sidebar becomes a horizontal strip: brand, links and the account foot
     on one scrolling line. Everything stays visible and Log out stays tappable,
     which is the property that matters — an earlier drawer-only design put the
     whole nav off-screen when its script failed to run, and a nav you cannot
     reach is worse than one that takes room.
     The height is the token, not a value, because the JS state below has to
     match it exactly. */
  .sidebar {
    flex-direction: row;
    align-items: center;
    gap: 12px;
    height: var(--navbar-h);
    padding: 0 12px;
    overflow-x: auto;
    overflow-y: hidden;
  }

  .sidebar .navgroup {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .sidebar .navgroup > .label {
    display: none;
  }

  .sidebar .foot {
    margin-top: 0;
    flex-direction: row;
    align-items: center;
    gap: 8px;
  }

  /* The account row is a .list in a column sidebar; on one line its row chrome
     is noise. */
  .sidebar .foot .list > li {
    min-height: 0;
    padding: 0;
    border-top: 0;
  }

  /* MOBILE NAV, UPGRADED STATE — the frozen behaviour script ran.
     The same sidebar becomes an off-canvas drawer and the injected .navbar
     takes its place in the flow, at the SAME token height. Nothing moves when
     the upgrade happens, however late the script arrives. */
  html[data-nav="drawer"] .navbar {
    display: flex;
  }

  /* The hamburger is the only way to the navigation on every screen where the
     drawer exists, so it takes the touch size across that whole range — not
     only below 560px, where the general button rule starts. Measured at 684px
     it was 38x28 before this. */
  html[data-nav="drawer"] .navtoggle {
    min-height: 44px;
    min-width: 44px;
    justify-content: center;
  }

  html[data-nav="drawer"] .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: 80%;
    max-width: 320px;
    height: auto;
    flex-direction: column;
    align-items: stretch;
    gap: 18px;
    padding: 14px 12px;
    overflow-x: hidden;
    overflow-y: auto;
    border-right: 1px solid var(--border);
    transform: translateX(-100%);
    transition: transform var(--dur) var(--ease);
    z-index: 40;
  }

  html[data-nav="drawer"] .sidebar .navgroup {
    display: block;
  }

  html[data-nav="drawer"] .sidebar .navgroup > .label {
    display: block;
  }

  html[data-nav="drawer"] .sidebar .foot {
    margin-top: auto;
    flex-direction: column;
    align-items: stretch;
  }

  html[data-nav="drawer"] .sidebar .foot .list > li {
    min-height: var(--row-h);
    padding: 10px 16px;
  }

  html[data-nav="drawer"][data-nav-open] .sidebar {
    transform: none;
  }

  html[data-nav="drawer"] .navscrim {
    display: block;
  }

  html[data-nav="drawer"][data-nav-open] .navscrim {
    opacity: 1;
    pointer-events: auto;
  }
}

/* Injected by the frozen behaviour script, never by the app. Both are
   display:none by default, so above the breakpoint they are not in the grid at
   all and the desktop shell is untouched — which is also what makes rotating
   past 860px safe: every rule that positions the drawer lives in the query, so
   crossing it puts the sidebar back in the flow with nothing left over. */
.navbar {
  display: none;
  align-items: center;
  gap: 10px;
  height: var(--navbar-h);
  padding: 0 12px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.navbar .brand {
  font-weight: 600;
}

.navscrim {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 39;
  background: color-mix(in srgb, var(--ink) 45%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}

/* The drawer is the first thing in this system that MOVES, so it is the first
   that has to offer a way out of the movement. Everything still works — the
   panel arrives and leaves, it simply does so at once.
   This also has a testing consequence worth knowing: a running transition
   outranks even !important, so an automated check that measures the panel's
   position has no way to read a settled value while one is in flight. With this
   here, --force-prefers-reduced-motion makes the measurement possible at all. */
@media (prefers-reduced-motion: reduce) {
  html[data-nav="drawer"] .sidebar,
  .navscrim {
    transition: none;
  }
}

/* PHONE. The 860px query above changes STRUCTURE — the sidebar folds, the hero
   stops being two columns. This one changes SCALE: the same layout, with the
   desktop's generosity taken out of it. 560px because it sits below the
   smallest tablet and above the largest phone, so nothing between the two
   queries is caught by both.
 * ================================================================== */

@media (max-width: 560px) {
  /* 22px of gutter on each side takes 12% of a 375px screen. */
  .content {
    padding: 16px;
  }

  /* A section's 72px of breathing room is a desktop measurement; on a phone it
     is most of a screen height between one paragraph and the next. Same
     selector as the resting rule, so it wins on source order rather than by
     inventing specificity. */
  body[data-genre="landing"] .band {
    padding: 44px 16px;
  }

  /* EVERY .row wraps here, not just the ones holding cards.
     That started as the narrower rule, on the evidence that a row of a brand,
     badges and buttons fitted on one line at 375px with zero overflow. It did —
     but that was ONE control-row shape, and it generalised badly. A gallery
     page put a five-item .segmented filter and a .search in the same row: 293px
     and 260px inside a 328px content box, and the page overflowed 229px. A row
     whose children cannot all shrink has to be allowed a second line. */
  .row {
    flex-wrap: wrap;
  }

  /* .grow gives a card flex-basis:0, so it shrinks forever and would never take
     the new line that flex-wrap offers it. */
  .row:has(> .card) > .card {
    flex-basis: 100%;
  }

  /* A search field wide enough to read a query in is a DESKTOP measurement.
     On a phone the field is the width of the screen, and the floor is the only
     thing stopping it from being that. */
  .search {
    min-width: 0;
  }

  /* Five filters on one line is 293px before the row's other child is counted.
     Wrapping keeps every option visible and reachable; scrolling the control
     would hide some of them behind a gesture with nothing to suggest it. */
  .segmented {
    flex-wrap: wrap;
  }

  /* Four real button labels measured 372px past a 360px screen. .row and
     .segmented were given a second line here; .actions is the same shape and
     was simply missed. */
  .actions {
    flex-wrap: wrap;
  }

  /* Same shape again, in the game genre. Note this one is NOT what its fixture
     needed: with overflow-wrap in place the six-slot HUD already fits, because
     its values can break. It is here because six slots sharing 360px are ~55px
     each whether or not they overflow, which the overflow check cannot see. */
  body[data-genre="game"] .hud {
    flex-wrap: wrap;
  }
}

/* Tap targets. 33px is a comfortable MOUSE target and too small for a finger;
   the usual floor is 44px. Applied on a coarse pointer as well as a narrow
   window, because a touch tablet is wide and still has no cursor — and NOT
   applied unconditionally, which would fatten every button on every desktop in
   the fleet to fix a problem desktops do not have. */
@media (max-width: 560px), (pointer: coarse) {
  button,
  .btn {
    min-height: 44px;
  }
}
