/* Star Realms web UI — single shared stylesheet.
 *
 * Architecture:
 *   - @layer order (low → high precedence): reset, tokens, base, layout, components, utilities
 *   - Design tokens are CSS custom properties on :root.
 *   - Light/dark via light-dark() + color-scheme.
 *   - Modern features only: native nesting, container queries, light-dark(), :has(), View Transitions.
 *   - See docs/web_ui/design_tokens.md for token documentation.
 *   - See docs/reference/latest_css_html.md for the broader feature reference.
 */

@view-transition { navigation: auto; }

/* Paint the view-transition snapshot pseudo-elements with the system canvas
 * color. Without this, the snapshot pseudos default to transparent, and the
 * cross-fade composites through whatever lies beneath — which is the canvas
 * during the transient gap between snapshots. Painting them opaque guarantees
 * the cross-fade composites against the active color scheme end-to-end.
 * (These pseudos live in the UA origin and are not @layer-scoped; declaring
 * here at top level is correct.) */
::view-transition-old(root),
::view-transition-new(root) {
  background-color: Canvas;
}

@layer reset, tokens, base, layout, components, utilities;

/* ---------- reset ---------- */
@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }
  body {
    margin: 0;
  }
  img,
  svg {
    max-inline-size: 100%;
    display: block;
  }
  button,
  input,
  select,
  textarea {
    font: inherit;
    color: inherit;
  }
  button {
    cursor: pointer;
  }
  a {
    color: inherit;
  }
  :focus:not(:focus-visible) {
    outline: none;
  }
}

/* ---------- tokens ---------- */
@layer tokens {
  :root {
    color-scheme: light dark;
    accent-color: light-dark(oklch(55% 0.18 260), oklch(72% 0.16 260));

    /* surface / text */
    --bg: light-dark(#f6f7fa, #0a0d14);
    --surface: light-dark(#ffffff, #131722);
    --surface-2: light-dark(#eef0f4, #1a1f2b);
    --text: light-dark(#1a1d24, #d8dce5);
    --text-muted: light-dark(#5a6068, #8b919c);
    --border: light-dark(#d8dce0, #232836);

    /* semantic */
    --accent: light-dark(oklch(55% 0.18 260), oklch(72% 0.16 260));
    --accent-fg: light-dark(#ffffff, #0a0d14);
    --danger: light-dark(oklch(55% 0.22 25), oklch(70% 0.20 25));
    --success: light-dark(oklch(50% 0.16 145), oklch(70% 0.16 145));

    /* spacing scale */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 1rem;
    --space-4: 1.5rem;
    --space-5: 2rem;
    --space-6: 3rem;

    /* font sizes */
    --text-sm: 0.875rem;
    --text-base: 1rem;
    --text-lg: 1.125rem;
    --text-xl: 1.5rem;
    --text-2xl: 2rem;
    --text-3xl: 3rem;

    /* radii */
    --radius-sm: 0.25rem;
    --radius: 0.5rem;
    --radius-lg: 1rem;

    /* transitions */
    --transition-fast: 150ms;
    --transition: 250ms;

    /* layering */
    --z-popup: 100;
    --z-toast: 200;
  }
}

/* ---------- base ---------- */
@layer base {
  html {
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: 16px;
    line-height: 1.5;
    /* `Canvas` / `CanvasText` are system color keywords; they auto-flip with
     * the active `color-scheme`. Painting the root with these keeps the
     * canvas opaque under both light and dark — no white flash during
     * cross-document or same-document View Transitions. See
     * docs/reference/dark_mode_flash.md. */
    background-color: Canvas;
    color: CanvasText;
  }

  body {
    background: var(--bg);
    color: var(--text);
    min-block-size: 100dvh;
    font-size: var(--text-base);
  }

  h1 {
    font-size: var(--text-3xl);
    text-wrap: balance;
    margin-block: 0 var(--space-4);
  }

  h2 {
    font-size: var(--text-2xl);
    text-wrap: balance;
    margin-block: 0 var(--space-3);
  }

  h3 {
    font-size: var(--text-xl);
    text-wrap: balance;
    margin-block: 0 var(--space-3);
  }

  p {
    text-wrap: pretty;
    margin-block: 0 var(--space-3);
  }

  :focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
  }
}

/* ---------- layout ---------- */
@layer layout {
  .page {
    /* No max-inline-size — use the full viewport. */
    inline-size: 100%;
    padding: var(--space-5) var(--space-4);
  }

  /* On phones, drop the side padding to give the board more breathing room. */
  @media (max-width: 30rem) {
    .page {
      padding: var(--space-3) var(--space-2);
    }
  }

  .landing {
    max-inline-size: 40rem;
    margin-inline: auto;
    padding-block: var(--space-6);
    text-align: center;
  }

  .stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
  }

  .row {
    display: flex;
    gap: var(--space-3);
    align-items: center;
  }
}

/* ---------- components ---------- */
@layer components {
  .button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--accent);
    color: var(--accent-fg);
    /* Tap-target floor: 44px is the iOS HIG / Android Material minimum. */
    min-block-size: 44px;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius);
    text-decoration: none;
    font-weight: 600;
    border: none;
    transition: opacity var(--transition-fast);

    &:hover {
      opacity: 0.88;
    }
  }

  .button-ghost {
    background: transparent;
    color: var(--accent);
    border: 1px solid var(--border);

    &:hover {
      background: var(--surface-2);
      opacity: 1;
    }
  }

  .card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
  }

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

/* ---------- components: game board ---------- */
@layer components {
  .game-board {
    container-type: inline-size;
    container-name: board;
  }

  /* Two-column shell: board on the left, actions/log column on the right,
   * with a draggable .splitter element between them.
   *
   * The split width lives on a `--right-col` custom property on the shell;
   * the JS splitter handler updates it on drag, and sessionStorage persists
   * it across body swaps. Default fallback keeps the right column at a
   * sensible width on first load. Each column scrolls independently via
   * `overflow-y: auto` + a bounded block size.
   */
  .game-shell {
    display: grid;
    grid-template-columns: 1fr 8px var(--right-col, min(420px, 36cqi));
    /* One explicit row that fills the container's definite block-size.
     * Without this, the implicit auto row is content-sized (default
     * align-content: start for grid), which makes `block-size: 100%` on
     * .board-column / .right-column fall back to auto (indefinite
     * containing block → percentage resolves to auto). That cascaded into
     * the inner panes never having bounded heights, so their overflow: auto
     * was a no-op and the window became the only scroller. */
    grid-template-rows: 1fr;
    column-gap: 0;
    row-gap: var(--space-4);
    block-size: calc(100dvh - 6rem);
    container-type: inline-size;
    container-name: shell;
  }

  .board-column {
    block-size: 100%;
    overflow-y: auto;
    padding-inline-end: var(--space-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
  }

  .splitter {
    inline-size: 8px;
    block-size: 100%;
    background: var(--border);
    cursor: col-resize;
    transition: background var(--transition-fast);
    /* Subtle inner indicator so the splitter is discoverable at rest.
     * Two faint vertical lines via a tiny gradient. */
    background-image: linear-gradient(
      to right,
      transparent 2px,
      var(--text-muted) 2px,
      var(--text-muted) 3px,
      transparent 3px,
      transparent 5px,
      var(--text-muted) 5px,
      var(--text-muted) 6px,
      transparent 6px
    );
    background-repeat: no-repeat;
    background-position: center;
    background-size: 8px 24px;

    &:hover,
    &.dragging {
      background-color: color-mix(in oklab, var(--accent), transparent 80%);
    }
  }

  /* Horizontal splitter between Action log and Actions panes. Same look as
   * the vertical splitter but rotated: row-resize cursor, horizontal dot
   * indicator. */
  .splitter-horizontal {
    inline-size: 100%;
    block-size: 8px;
    cursor: row-resize;
    background-image: linear-gradient(
      to bottom,
      transparent 2px,
      var(--text-muted) 2px,
      var(--text-muted) 3px,
      transparent 3px,
      transparent 5px,
      var(--text-muted) 5px,
      var(--text-muted) 6px,
      transparent 6px
    );
    background-size: 24px 8px;
  }

  .right-column {
    block-size: 100%;
    padding-inline-start: var(--space-3);
    /* Three rows: log-pane, horizontal splitter, actions-pane. The log
     * pane's height lives on --log-height; the actions pane fills the rest. */
    display: grid;
    grid-template-rows: var(--log-height, 30%) 8px 1fr;
    row-gap: 0;
    overflow: hidden;
    min-inline-size: 16rem;
  }

  .right-pane {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-3);
    display: flex;
    flex-direction: column;
    min-block-size: 0;
    /* No overflow here — the inner list (.log-list / .action-list) is the
     * scroller, so the H2 heading stays put while entries scroll
     * underneath. */
  }

  /* On narrow viewports, collapse to a single stacked column.
   * The splitter is hidden — drag-to-resize doesn't make sense when
   * everything is stacked vertically. The grid-template-rows override
   * is paired with the .game-shell base rule above (1fr → auto here)
   * so .right-column / .board-column become content-sized and the
   * window takes over as the scroller. */
  @container (max-width: 60rem) {
    .game-shell {
      grid-template-columns: 1fr;
      grid-template-rows: auto;
      block-size: auto;
    }
    /* Neither splitter makes sense once everything is stacked vertically. */
    .splitter,
    .splitter-horizontal {
      display: none;
    }
    .board-column, .right-column {
      block-size: auto;
      overflow: visible;
      padding-inline: 0;
    }
    /* The right column's grid (log / splitter / actions) collapses to a
     * simple flex stack: the window is the scroller, so the panes size to
     * their content and every action is visible and tappable. */
    .right-column {
      display: flex;
      flex-direction: column;
      gap: var(--space-4);
    }
    /* Inner lists stop being bounded flex scrollers. The action list shows
     * all actions at full height; the action log keeps its own scroll but is
     * capped so it never dominates a small screen. */
    .action-list {
      flex: none;
      overflow: visible;
    }
    .log-list {
      flex: none;
      max-block-size: 45vh;
    }
  }

  .resources-header {
    justify-content: space-between;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;

    & h2 {
      margin: 0;
    }
  }

  .deck-buttons {
    gap: var(--space-2);
    flex-wrap: wrap;
  }

  /* On very narrow boards (phone portrait), let the deck buttons take a
   * full row of their own below the heading instead of competing for space. */
  @container shell (max-width: 30rem) {
    .deck-buttons {
      flex-basis: 100%;
    }
  }

  .game-footer {
    margin-block-start: var(--space-5);
    padding-block: var(--space-4);
    border-block-start: 1px solid var(--border);
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3);

    & h1 {
      margin: 0;
      font-size: var(--text-xl);
    }
  }

  /* Small "Opponent: V4 — ..." line above the resources. */
  .opponent-banner {
    margin-block: 0 var(--space-2);
  }

  /* Opponent policy selector in the New Game footer form. */
  .new-game-form {
    gap: var(--space-3);
    flex-wrap: wrap;
  }

  .opponent-select {
    display: inline-flex;
    flex-direction: column;
    gap: var(--space-1);
    font-size: var(--text-sm);

    & .opponent-select-label {
      color: var(--text-muted);
      text-transform: uppercase;
      letter-spacing: 0.06em;
      font-size: 0.75rem;
    }

    & select {
      min-block-size: 44px;
      padding: var(--space-2) var(--space-3);
      border-radius: var(--radius);
      border: 1px solid var(--border);
      background: var(--surface-2);
      color: var(--text);
    }
  }

  /* Opponent selector on the landing hero (inside the .hero-link-form, which
   * is `display: contents`, so this label participates in the hero-links
   * flex column directly). */
  .hero-opponent-select {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    text-align: start;
    color: #97a6c4;
    font-size: var(--text-sm);

    & select {
      inline-size: 100%;
      min-block-size: 44px;
      padding: var(--space-3) var(--space-4);
      border-radius: var(--radius);
      border: 1px solid rgba(150, 170, 220, 0.22);
      background: rgba(20, 30, 60, 0.55);
      color: #e8ecf5;
      font-weight: 600;
    }
  }

  .banner {
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius);
    font-weight: 600;

    &.success {
      background: color-mix(in oklab, var(--success), transparent 80%);
      color: var(--success);
    }

    &.danger {
      background: color-mix(in oklab, var(--danger), transparent 80%);
      color: var(--danger);
    }
  }

  .resources-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: grid;
    gap: var(--space-1);

    & .warn {
      color: var(--danger);
      font-weight: 600;
    }
  }

  /* Four stat tiles: Your Authority, Opponent Authority, Trade, Combat.
   * Each tile has its own accent via per-tile custom props — value text and
   * inner glow inherit from --stat-accent / --stat-glow. Auto-fit + minmax
   * lets it collapse to 2 columns, then 1, on narrow boards. */
  .stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: var(--space-3);
    margin-block: var(--space-3);
  }

  .stat {
    position: relative;
    display: grid;
    grid-template-rows: auto auto;
    justify-items: center;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-2);
    border-radius: var(--radius-lg);
    background: light-dark(#f3f5fa, #161c2a);
    border: 1px solid color-mix(in oklab, var(--stat-accent, var(--border)), transparent 70%);
    overflow: hidden;
    isolation: isolate;
  }

  /* Soft radial wash behind the value — uses the per-tile glow color so each
   * stat has a distinctly tinted backdrop without becoming overwhelming. */
  .stat::before {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(
      ellipse at 50% 30%,
      var(--stat-glow, transparent) 0%,
      transparent 65%
    );
    opacity: 0.35;
    z-index: -1;
    pointer-events: none;
  }

  .stat-value {
    font-size: clamp(2rem, 5cqi, 2.75rem);
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: var(--stat-accent, var(--text));
    text-shadow:
      0 0 12px color-mix(in oklab, var(--stat-glow, transparent), transparent 40%),
      0 0 2px color-mix(in oklab, var(--stat-accent, transparent), transparent 60%);
    letter-spacing: -0.02em;
  }

  .stat-label {
    font-size: var(--text-sm);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    text-align: center;
    line-height: 1.1;
  }

  .stat--authority-mine {
    /* Green: yours, healthy. */
    --stat-accent: light-dark(oklch(52% 0.16 150), oklch(78% 0.16 150));
    --stat-glow: light-dark(oklch(80% 0.18 150), oklch(55% 0.22 150));
  }

  .stat--authority-theirs {
    /* Red: the target. */
    --stat-accent: light-dark(oklch(55% 0.22 25), oklch(75% 0.20 25));
    --stat-glow: light-dark(oklch(80% 0.22 25), oklch(55% 0.25 25));
  }

  .stat--trade {
    /* Gold. */
    --stat-accent: light-dark(oklch(60% 0.16 80), oklch(85% 0.16 85));
    --stat-glow: light-dark(oklch(85% 0.18 80), oklch(62% 0.20 80));
  }

  .stat--combat {
    /* Orange. */
    --stat-accent: light-dark(oklch(60% 0.20 45), oklch(80% 0.18 45));
    --stat-glow: light-dark(oklch(82% 0.20 45), oklch(55% 0.25 45));
  }

  /* Subtle pulse on the trade/combat values whenever they're non-zero —
   * encoded via a sibling-less heuristic: the value text reads `0` as plain
   * grey instead of accent, so authoring it via CSS would require :has().
   * For now, the always-on glow is enough; the accent already pops. */

  .board-side {
    padding: var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);

    & h2 {
      font-size: var(--text-lg);
      margin-block: 0 var(--space-2);
    }
  }

  .card-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding-block: var(--space-2);
  }

  .game-card {
    position: relative;
    /* Floor of 60px keeps cards tappable on a 360px viewport. 14cqi scales
     * with the nearest inline-size container — `.game-shell` on the main
     * board, or the popover itself when inside a deck-viewer popup. */
    inline-size: clamp(60px, 14cqi, 140px);
    aspect-ratio: 5 / 7; /* portrait — ships */
    background: var(--surface-2);
    border-radius: var(--radius);
    overflow: hidden;
    display: flex;
    flex-direction: column;

    /* Bases (Outposts and non-Outpost bases) are printed in landscape
     * orientation; the card images are 420x300. Width bumped well past
     * the ship size so the artwork reads — bases are the persistent,
     * board-relevant cards, so giving them more pixels is the right call. */
    &.base {
      aspect-ratio: 7 / 5;
      inline-size: clamp(120px, 28cqi, 320px);
    }
  }

  /* When the .game-card is itself an interactive button (hand cards,
   * affordable trade-row cards). */
  .card-button {
    border: none;
    padding: 0;
    cursor: pointer;
    transition: transform var(--transition-fast);

    &:hover {
      transform: translateY(-2px);
    }

    &:focus-visible {
      outline: 2px solid var(--accent);
      outline-offset: 2px;
    }
  }

  .card-form {
    margin: 0;
  }

  /* Unaffordable trade-row cards: not a form button, just a styled <button>
   * that flashes red on click. */
  .card-button.unaffordable {
    cursor: not-allowed;
    opacity: 0.65;

    &:hover {
      transform: none;
    }

    &:active {
      animation: flash-red 280ms;
      opacity: 1;
    }
  }

  @keyframes flash-red {
    0%, 100% {
      outline: 2px solid transparent;
      outline-offset: -2px;
    }
    20%, 60% {
      outline: 3px solid var(--danger);
      outline-offset: -2px;
    }
  }

  /* Action log: oldest-first list inside .log-pane. The list itself is
   * the scroll container (not the parent pane), so the "Action log"
   * heading stays put while entries scroll below. flex: 1 + min-block-size: 0
   * lets the list shrink to the remaining flex space; align-content: end
   * pins the items to the bottom of the list when they fit. main.js scrolls
   * the list to scrollHeight after every swap so the latest entry is
   * visible by default. */
  .log-list {
    list-style: none;
    padding: 0;
    margin: 0;
    flex: 1 1 0;
    min-block-size: 0;
    overflow-y: auto;
    display: grid;
    gap: var(--space-1);
    align-content: end;
  }

  .log-entry {
    display: grid;
    grid-template-columns: auto auto 1fr;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);

    &:nth-child(odd) {
      background: var(--surface-2);
    }

    /* Most-recent entry highlights briefly when added (oldest-first list,
     * so newest is :last-child). */
    &:last-child {
      animation: log-flash 600ms ease-out;
    }
  }

  @keyframes log-flash {
    0% {
      background: color-mix(in oklab, var(--accent), transparent 70%);
    }
    100% {
      background: var(--surface-2);
    }
  }

  /* Synthetic "You/Opponent starts turn N" row injected after every EndTurn.
   * Renders as a centered divider — visually distinct from real actions so
   * it's clear at a glance that a new turn began. */
  .log-entry-marker {
    grid-template-columns: 1fr;
    text-align: center;
    font-style: italic;
    font-weight: 600;
    color: var(--text-muted);
    background: transparent !important;
    padding-block: var(--space-2);
    margin-block: var(--space-1);
    position: relative;
    animation: none;

    &::before,
    &::after {
      content: "";
      position: absolute;
      inset-block-start: 50%;
      inline-size: 25%;
      block-size: 1px;
      background: linear-gradient(
        to right,
        transparent,
        color-mix(in oklab, var(--accent), transparent 60%)
      );
    }
    &::before {
      inset-inline-start: 0;
    }
    &::after {
      inset-inline-end: 0;
      background: linear-gradient(
        to left,
        transparent,
        color-mix(in oklab, var(--accent), transparent 60%)
      );
    }
  }

  .log-marker-text {
    color: color-mix(in oklab, var(--accent), var(--text));
    letter-spacing: 0.03em;
  }

  .log-turn {
    font-family: ui-monospace, "SF Mono", monospace;
    color: var(--text-muted);
    font-weight: 600;
  }

  .log-actor {
    color: var(--text-muted);
    text-transform: capitalize;
  }

  /* Human-readable state dump in the WM7 details panel. */
  .state-readable {
    white-space: pre-wrap;
    font-family: ui-monospace, "SF Mono", monospace;
    font-size: var(--text-sm);
    line-height: 1.45;
  }

  /* Deck-viewer popovers (Your Deck / Opponent's Deck). Wider than the
   * play-choice popover because they show many cards. */
  .deck-popover {
    inline-size: min(70rem, 95vw);
    max-block-size: 90vh;
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    box-shadow: 0 12px 32px rgb(0 0 0 / 0.35);
    overflow-y: auto;
    /* Establish an inline-size container so the `.game-card`s inside scale
     * to the popover, not to the page behind it. */
    container-type: inline-size;

    & h3 { margin-block: 0 var(--space-2); }
    & h4 {
      margin-block: var(--space-3) var(--space-2);
      font-size: var(--text-base);
      color: var(--text-muted);
    }

    &::backdrop {
      background: rgb(0 0 0 / 0.5);
      backdrop-filter: blur(2px);
    }

    @starting-style {
      opacity: 0;
      transform: scale(0.98);
    }
    transition: opacity var(--transition-fast), transform var(--transition-fast);
  }

  /* Play-choice popover (Patrol Mech etc.). Top-layer via the popover API. */
  .play-popover {
    inline-size: min(28rem, 90vw);
    max-block-size: 80vh;
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    box-shadow: 0 12px 32px rgb(0 0 0 / 0.35);
    container-type: inline-size;

    & h3 {
      margin-block: 0 var(--space-3);
    }

    &::backdrop {
      background: rgb(0 0 0 / 0.45);
      backdrop-filter: blur(2px);
    }

    /* Entry animation when the popover opens. */
    @starting-style {
      opacity: 0;
      transform: scale(0.96);
    }
    transition: opacity var(--transition-fast), transform var(--transition-fast);
  }

  .card-image {
    inline-size: 100%;
    block-size: 100%;
    /* `contain` shows the whole image inside the slot (with thin background
     * letterbox bars only if the slot's aspect ratio ever drifts from the
     * image's; for ships 5:7 and bases 7:5 the slots match the JPGs and
     * there's no bar at all). `cover` was cropping when the badges
     * overlapped the artwork — visible loss of card text. */
    object-fit: contain;
    display: block;
  }

  .game-card-meta {
    position: absolute;
    inset-block-end: 0;
    inset-inline-end: 0;
    background: color-mix(in oklab, var(--bg), transparent 20%);
    color: var(--text);
    font-size: var(--text-sm);
    font-weight: 600;
    padding: 2px 6px;
    border-start-start-radius: var(--radius-sm);
  }

  .explorer-pile {
    margin-block-start: var(--space-2);
    font-size: var(--text-sm);
    color: var(--text-muted);
  }

  .small {
    font-size: var(--text-sm);
  }

  /* Action list — single column, top-to-bottom at all sizes. Like
   * .log-list, it's the scroller inside .actions-pane so the "Actions"
   * heading stays put. */
  .action-list {
    list-style: none;
    padding: 0;
    margin: 0;
    flex: 1 1 0;
    min-block-size: 0;
    overflow-y: auto;
    display: grid;
    gap: var(--space-2);
    grid-template-columns: 1fr;
  }

  .action-button {
    inline-size: 100%;
    /* Tap-target floor matches .button. */
    min-block-size: 44px;
    text-align: start;
    background: var(--surface-2);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-2) var(--space-3);
    display: flex;
    align-items: center;
    gap: var(--space-3);
    transition: background var(--transition-fast), border-color var(--transition-fast);

    &:hover {
      background: var(--surface);
      border-color: var(--accent);
    }

    & kbd {
      display: inline-block;
      min-inline-size: 1.5em;
      text-align: center;
      padding: 2px 6px;
      border-radius: var(--radius-sm);
      background: var(--bg);
      border: 1px solid var(--border);
      font-family: ui-monospace, "SF Mono", monospace;
      font-size: var(--text-sm);
      font-weight: 600;
    }
  }
}

/* ---------- components: starfield pages ---------- */
@layer components {
  /* Force dark canvas before CSS even paints, so the no-flash invariant in
   * docs/reference/dark_mode_flash.md still holds. `:has()` on <html> lets
   * us key off the body class set by Askama's `body_class` block. */
  html:has(body.starfield-page) {
    color-scheme: dark;
    background: #04060d;
  }

  body.starfield-page {
    color-scheme: dark;
    color: #e8ecf5;
    min-block-size: 100dvh;
    /* Three soft nebula washes on top of a near-black base. `fixed`
     * attachment keeps the wash anchored to the viewport during scroll. */
    background:
      radial-gradient(ellipse at 18% 22%, rgba(95, 60, 160, 0.22), transparent 55%),
      radial-gradient(ellipse at 82% 78%, rgba(40, 90, 180, 0.20), transparent 55%),
      radial-gradient(ellipse at 50% 110%, rgba(140, 50, 110, 0.12), transparent 50%),
      #04060d;
    background-attachment: fixed;
  }

  /* Two pseudo-element star layers tiled across the viewport. Static (no
   * motion) per the page's design choice — each radial-gradient is one star
   * positioned within a single tile, and `background-repeat: repeat` tiles
   * that pattern across the screen. */
  body.starfield-page::before,
  body.starfield-page::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background-repeat: repeat;
  }

  /* Back layer: many small dim stars. */
  body.starfield-page::before {
    background-image:
      radial-gradient(1px 1px at 23px 41px, #ffffffcc, transparent 1.5px),
      radial-gradient(1px 1px at 73px 137px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 110px 250px, #cdd6f4aa, transparent 1.5px),
      radial-gradient(1px 1px at 195px 64px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 145px 380px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 285px 180px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 340px 90px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 460px 220px, #fdf6e3aa, transparent 1.5px),
      radial-gradient(1px 1px at 50px 320px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 250px 360px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 410px 410px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 90px 470px, #cdd6f4aa, transparent 1.5px),
      radial-gradient(1px 1px at 330px 490px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 480px 60px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 200px 200px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 390px 300px, #ffffffaa, transparent 1.5px),
      radial-gradient(1px 1px at 30px 220px, #cdd6f4aa, transparent 1.5px),
      radial-gradient(1px 1px at 430px 460px, #ffffffaa, transparent 1.5px);
    background-size: 500px 500px;
  }

  /* Front layer: fewer, brighter stars with a hint of color. */
  body.starfield-page::after {
    background-image:
      radial-gradient(1.5px 1.5px at 60px 90px, #ffffff, transparent 2px),
      radial-gradient(2px 2px at 240px 30px, #ffffff, transparent 2.5px),
      radial-gradient(1.5px 1.5px at 170px 230px, #cdd6f4, transparent 2px),
      radial-gradient(2.5px 2.5px at 380px 140px, #ffffff, transparent 3px),
      radial-gradient(1.5px 1.5px at 90px 290px, #fdf6e3, transparent 2px),
      radial-gradient(2px 2px at 330px 380px, #ffffff, transparent 2.5px),
      radial-gradient(1.5px 1.5px at 480px 320px, #ffffff, transparent 2px),
      radial-gradient(2px 2px at 540px 80px, #cdd6f4, transparent 2.5px),
      radial-gradient(1.5px 1.5px at 610px 240px, #ffffff, transparent 2px),
      radial-gradient(2.5px 2.5px at 700px 460px, #ffffff, transparent 3px),
      radial-gradient(1.5px 1.5px at 200px 460px, #ffffff, transparent 2px),
      radial-gradient(2px 2px at 420px 540px, #fdf6e3, transparent 2.5px),
      radial-gradient(1.5px 1.5px at 660px 600px, #cdd6f4, transparent 2px),
      radial-gradient(2px 2px at 130px 640px, #ffffff, transparent 2.5px);
    background-size: 720px 720px;
  }

  /* Lift content above the star pseudo-elements. */
  body.starfield-page > * {
    position: relative;
    z-index: 1;
  }

  /* Hero layout: centered, full-viewport, large title and link stack. */
  .hero {
    max-inline-size: 36rem;
    margin-inline: auto;
    padding: clamp(2rem, 6vh, 5rem) var(--space-4);
    min-block-size: 100dvh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: var(--space-4);
    text-align: center;

    & h1 {
      font-size: clamp(2.5rem, 9vw, 4rem);
      letter-spacing: -0.02em;
      margin: 0;
      background: linear-gradient(180deg, #ffffff 0%, #b8c4ff 100%);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
    }
  }

  .hero-tagline {
    color: #97a6c4;
    font-size: var(--text-lg);
    margin: 0 0 var(--space-3);
  }

  .hero-links {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
  }

  /* Form wrapping the "Debug RL Engine" button collapses in the flex flow
   * so its button sits in line with the sibling <a> elements. */
  .hero-link-form {
    margin: 0;
    display: contents;
  }

  .hero-link {
    display: block;
    inline-size: 100%;
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius);
    text-decoration: none;
    font-weight: 600;
    text-align: center;
    border: 1px solid transparent;
    cursor: pointer;
    font: inherit;
    font-weight: 600;
    transition:
      transform var(--transition-fast),
      background var(--transition-fast),
      border-color var(--transition-fast),
      box-shadow var(--transition-fast);
  }

  .hero-link-primary {
    background: linear-gradient(135deg, #4a5dff 0%, #7a4dff 100%);
    color: #ffffff;
    font-size: var(--text-lg);
    padding-block: var(--space-4);
    box-shadow: 0 6px 24px rgba(90, 90, 220, 0.45);

    &:hover {
      transform: translateY(-1px);
      box-shadow: 0 10px 32px rgba(110, 110, 240, 0.6);
    }
  }

  .hero-link-secondary {
    background: rgba(20, 30, 60, 0.55);
    color: #e8ecf5;
    border-color: rgba(150, 170, 220, 0.22);
    backdrop-filter: blur(6px);

    &:hover {
      background: rgba(40, 55, 95, 0.7);
      border-color: rgba(160, 180, 230, 0.55);
      transform: translateY(-1px);
    }
  }

  /* Official store badges for the digital app, sitting in the hero link
   * stack below the buy buttons. Apple and Google links use their required
   * badge artwork (self-hosted under /static/badges/); Steam has no public
   * badge program, so it gets a hand-built badge matching Apple's geometry. */
  .hero-digital {
    margin: var(--space-2) 0 0;
    color: #97a6c4;
    font-size: var(--text-sm);
  }

  .store-badges {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: var(--space-2) var(--space-3);

    & a {
      display: block;
      transition: transform var(--transition-fast);

      &:hover {
        transform: translateY(-1px);
      }
    }

    & img {
      display: block;
      block-size: 46px;
      inline-size: auto;
    }

    /* Google's badge PNG bakes a transparent margin into the artwork;
     * oversize it a touch so the visible badge matches its neighbors. */
    & .badge-google {
      block-size: 52px;
    }
  }

  .badge-steam {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
    box-sizing: border-box;
    block-size: 46px;
    padding-inline: var(--space-3);
    border-radius: 7px; /* Apple's badge radius, scaled to 46px tall */
    background: #000;
    border: 1px solid #a6a6a6; /* matches the Apple badge outline */
    color: #fff;
    text-decoration: none;
    line-height: 1.15;
    text-align: left;
  }

  .badge-steam-kicker {
    font-size: 0.6rem;
  }

  .badge-steam-name {
    font-size: 1.25rem;
    font-weight: 600;
    letter-spacing: 0.01em;
  }

  /* Research page layout — comfortable reading width over the starfield. */
  .research {
    max-inline-size: 42rem;
    margin-inline: auto;
    padding: clamp(2rem, 6vh, 5rem) var(--space-4);
    min-block-size: 100dvh;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);

    & h1 {
      font-size: clamp(2rem, 6vw, 3rem);
      margin: 0 0 var(--space-3);
      background: linear-gradient(180deg, #ffffff 0%, #b8c4ff 100%);
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
    }

    & p {
      font-size: var(--text-lg);
      line-height: 1.65;
      color: #d8dce5;
    }

    & a {
      color: #b8c4ff;
      text-decoration: underline;
      text-underline-offset: 3px;

      &:hover {
        color: #d8e0ff;
      }
    }
  }

  .research-back {
    font-size: var(--text-sm);
    margin: 0 0 var(--space-2);
  }

  /* RL Lessons — long-form lesson pages share the research reading column,
   * plus headings, lists, and hand-rolled math (no MathJax/KaTeX per
   * docs/adr_frontend_standards.md: HTML + Unicode + a math serif stack). */
  .lesson {
    & h2 {
      font-size: var(--text-xl);
      color: #e8ecf5;
      margin: var(--space-4) 0 0;
    }

    & ul,
    & ol {
      font-size: var(--text-lg);
      line-height: 1.65;
      color: #d8dce5;
      margin: 0;
      padding-inline-start: 1.5em;
      display: flex;
      flex-direction: column;
      gap: var(--space-2);
    }

    & code {
      font-size: 0.875em;
      background: rgba(20, 30, 60, 0.55);
      border: 1px solid rgba(150, 170, 220, 0.22);
      border-radius: var(--radius-sm);
      padding: 0.05em 0.35em;
    }

    /* Code listings (the exercise stub and solution on lesson pages).
     * The inline-code chrome moves to the <pre>; nested <code> resets. */
    & pre {
      font-size: var(--text-sm);
      line-height: 1.55;
      background: rgba(20, 30, 60, 0.55);
      border: 1px solid rgba(150, 170, 220, 0.22);
      border-radius: var(--radius-sm);
      padding: var(--space-2);
      overflow-x: auto;
      color: #d8dce5;

      & code {
        font-size: 1em;
        background: none;
        border: none;
        padding: 0;
      }
    }

    /* Math variables, inline and in display equations. */
    & var,
    & .math {
      font-family: "STIX Two Math", "Cambria Math", Georgia, "Times New Roman", serif;
      font-style: italic;
      white-space: nowrap;
    }

    /* Side-by-side comparison table (e.g. value iteration vs TD(0)). */
    & .td-table {
      width: 100%;
      border-collapse: collapse;
      font-size: var(--text-base);
      color: #d8dce5;
      margin-block: var(--space-3);

      & th,
      & td {
        border: 1px solid rgba(150, 170, 220, 0.22);
        padding: var(--space-2);
        text-align: left;
        vertical-align: top;
      }

      & th {
        color: #e8ecf5;
        background: rgba(20, 30, 60, 0.55);
      }
    }
  }

  .lesson-list {
    list-style: none;
    padding-inline-start: 0;
  }

  /* Display equation: centered, math serif. Atomic runs (.m spans) never
   * break internally; on narrow screens the equation wraps *between* runs —
   * after "=", between major terms — like a textbook two-line equation.
   * overflow-x is a safety net for any run still wider than the viewport. */
  .eqn {
    font-family: "STIX Two Math", "Cambria Math", Georgia, "Times New Roman", serif;
    font-size: var(--text-xl);
    text-align: center;
    color: #e8ecf5;
    margin-block: var(--space-2);
    padding-block: var(--space-1);
    overflow-x: auto;

    & .m {
      white-space: nowrap;
    }

    /* Big operators (∑, ∫) sit larger than surrounding symbols. */
    & .op {
      font-size: 1.4em;
      line-height: 1;
      vertical-align: -0.2em;
      font-style: normal;
    }

    /* The "=?" in the deliberately-wrong definition. */
    & .qmark {
      color: var(--danger);
    }
  }
}

/* ---------- components: experiment index lists ---------- */
@layer components {
  .exp-index {
    list-style: none;
    padding-inline-start: 0;
    display: grid;
    gap: var(--space-3);
  }
  .exp-index li {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-3);
  }
  .exp-index a {
    color: var(--accent);
    text-decoration: none;
  }
  .exp-index a:hover {
    text-decoration: underline;
  }
}

/* ---------- utilities ---------- */
@layer utilities {
  .visually-hidden {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}
