/*
 * 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;
  --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;
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.5;
  background: var(--canvas);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

/* 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;
}

a:hover {
  color: var(--accent);
}

p a,
.empty a,
.share-sub a {
  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;
}

.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;
}

.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;
}

.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,
.btn:hover {
  background: color-mix(in srgb, var(--ink) 88%, #fff);
}

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);
}

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

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

button:disabled,
.btn:disabled {
  opacity: 0.5;
  cursor: default;
}

.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;
}

input,
textarea,
select {
  width: 100%;
  padding: 8px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font-family: inherit;
  font-size: 13px;
}

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;
}

.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;
  }

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

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

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