/* ============================================================
   layout.css — the four layout primitives
   ------------------------------------------------------------
   docs/plans/spacing-and-layout-programme-2026-09-29.md, Task L0.

   Imported by base.css and by admin-shell.css, after primitives.css
   (which is after tokens.css and aliases.css), so it reaches all
   three bundles and sits below every bespoke sheet in the cascade —
   the same position primitives.css holds.

   A sibling sheet, not a section of primitives.css: primitives.css
   is 828 lines today (design-system/README.md's own note says
   nothing in tests/convention gates a .css file's length, but this
   programme holds itself to the budget anyway), and eight more
   rules would have pushed it further past 600 rather than under it.

   Three rules, and they are primitives.css's own:

   1. Nothing lands here without the commit also deleting every
      rule it replaces and moving the markup — that is wave 2
      (LA1-LA3), not this task. L0 adopts nothing.
   2. Tokens only. Every default reads a --space-* token; the one
      exception, --grid-min's 240px, is a width fallback, not
      spacing, and is confirmed out of scope for both
      tests/convention/css-raw-values.test.js (font size, radius,
      colour only) and the spacing guard (margin, padding, gap and
      scroll-padding/margin only — never grid-template-columns).
   3. Everything here is in `@layer primitives`, the same layer
      primitives.css declares into. Cascade layers merge by name
      across every `@layer primitives { … }` block a bundle loads,
      so this sheet joins the one layer base.css and admin-shell.css
      already opened with `@layer reset, primitives;` — it does not
      open a second layer. Bespoke sheets are unlayered and win
      without a specificity contest; a rule that sets display,
      flex-direction, flex-wrap, grid-template-columns (or a shorthand
      of them) or gap on one of these classes outside this layer —
      or on a bespoke class the markup puts one of them beside —
      would silently beat it (tests/convention/css-layers-layout.test.js
      and css-layers-layout-adoptions.test.js).
   ============================================================ */

@layer primitives {
  /* --- Stack -------------------------------------------------------------
     A vertical flow: every child, one under the next, with a consistent
     gap between them. The default gap is --space-1 (4px, the layout-rulings
     item 1 change) — the inventory's own count of stack call sites found
     more relying on the tighter gap than the looser one, so the default
     follows the majority and the 11 minority sites (the inventory's
     default-reliant classes) get an explicit --stack-gap: var(--gap-related)
     instead; a context that wants a different rhythm still sets --stack-gap
     on the element rather than writing a bespoke flex-column rule — 101 of
     the 141 stack rules the 2026-09-29 layout inventory found were six
     declarations or fewer, which is this primitive and nothing else. */
  .stack {
    display: flex;
    flex-direction: column;
    gap: var(--stack-gap, var(--gap-tight));
  }

  /* --- Cluster -------------------------------------------------------------
     A horizontal run that wraps rather than overflows: items that do not
     fit the row drop to the next one instead of pushing past the edge —
     the shape a non-wrapping row got wrong at 320 (the inventory's
     "#whatField at 320" regression). align-items: center keeps mixed-height
     children (an icon beside text) on one baseline-ish line. The default
     gap is --space-2 (8px), the tighter of the two gaps in this file
     because a cluster's items are usually small (a pill, a chip, a short
     label) rather than the stack's larger blocks. */
  .cluster {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--cluster-gap, var(--gap-close));
  }
  /* Pushes the last child to the row's far edge — the inline-row shape the
     inventory found 21 rules of, folded in as a modifier rather than a
     fifth primitive. */
  .cluster--between { justify-content: space-between; }
  /* Opts out of wrapping for a cluster whose row must never break —
     .ws-check-group-head's shape (folds as a cluster, per the inventory,
     because only its gap and wrap change across the breakpoint). */
  .cluster--nowrap { flex-wrap: nowrap; }

  /* --- Split -------------------------------------------------------------
     Two regions: one fixed-width track and one fluid track that takes
     whatever is left. `minmax(0, 1fr)`, not a bare `1fr` — a bare `1fr`
     track cannot shrink below its content's intrinsic width, which is the
     grid analogue of the flex `min-width: 0` note primitives.css:.field
     carries, and without it a long value in the fluid track pushes the
     fixed track off the row rather than wrapping. --split-fixed defaults
     to `auto` (the fixed side sizes to its own content) and --split-gap to
     --space-4 (16px). Not `.sidebar` — that name is taken by the app
     shell's own rail (shell.css:.shell, deliberately not folded here; see
     the plan's Review focus §4). */
  .split {
    display: grid;
    grid-template-columns: var(--split-fixed, auto) minmax(0, 1fr);
    gap: var(--split-gap, var(--gap-group));
    align-items: start;
  }
  /* The fixed track trailing instead of leading — portal.css's
     `.accepting-row` (1fr auto) is this shape. */
  .split--end { grid-template-columns: minmax(0, 1fr) var(--split-fixed, auto); }

  /* --- Grid ----------------------------------------------------------------
     Equal-width columns, the shape behind style.css's `.directory-grid`,
     `.fp-steps`, `.trust-ladder` and the six other repeat(N, …) rules the
     inventory found. --grid-cols defaults to 2 (the inventory's most common
     column count); a context that wants three or five sets --grid-cols on
     the element. --grid-gap defaults to --space-4 (16px), matching .split's
     gap so a page mixing the two primitives reads one rhythm. */
  .grid {
    display: grid;
    grid-template-columns: repeat(var(--grid-cols, 2), minmax(0, 1fr));
    gap: var(--grid-gap, var(--gap-group));
  }
  /* Column count set by available width instead of a fixed number — the
     three auto-fit/auto-fill rules the inventory found (.filter-island,
     .publish-checklist, .verify-list). --grid-min is a WIDTH, not a
     spacing value, so it is not a --space-* token and the spacing guard
     does not read it; 240px is the inventory's own figure for the
     narrowest of the three (.filter-island). `min(var(--grid-min, 240px),
     100%)` is what collapses this to one column on a viewport narrower
     than the minimum itself — 320px would otherwise overflow a 240px-wide
     single track plus its grid gap on some phones. */
  .grid--auto { grid-template-columns: repeat(auto-fit, minmax(min(var(--grid-min, 240px), 100%), 1fr)); }

  /* --- The intrinsic primitives -------------------------------------------
     Three shapes that change with the width of their OWN box rather than
     the viewport's, so a component can sit in a 320px phone column or a
     565px console pane and lay itself out for the space it actually has —
     what a viewport @media cannot know. The intent-layer programme's
     breakpoint inventory (2026-09-29) replaces 43 @media blocks with these
     and container queries; its Task T2 lands them and adopts none.

     Each reads a public hook on the element (--split-min, --switcher-at,
     --grid-max) and turns it into a private --_* property ON THAT ELEMENT.
     The children read the private one. That is the point of the
     indirection: the hook reset below sets every public hook back to
     `initial` on a primitive's children, so a child rule that read
     var(--split-fixed) itself would see its own reset value and fall back
     to `auto`. A custom property's var() is substituted where it is
     declared, so --_split-basis is already `200px` when the child inherits
     it (tests/boot/layout-primitives.spec.js proves it with the child's
     computed flex-basis). Known limit: a nested .split--wrap or .switcher
     that is itself the DIRECT child of another redeclares the private
     property for its own children, so its basis in the outer one is its
     own hook, not the outer's. Set the hook on the inner element too. */

  /* The sidebar pattern: .split's two regions side by side while the fluid
     one keeps at least --split-min (default 50%) of the row beside the
     fixed one's --split-fixed basis, and one above the other once it
     cannot — no @media. Used with .split, which supplies the gap:
     `class="split split--wrap"`. With .split--end the fixed region trails. */
  .split--wrap {
    display: flex;
    flex-wrap: wrap;
    --_split-basis: var(--split-fixed, auto);
    --_split-min: min(var(--split-min, 50%), 100%);
  }
  .split--wrap > * { flex: 1 1 var(--_split-basis); }
  .split--wrap > :last-child { flex: 999 1 0%; min-inline-size: var(--_split-min); }
  .split--wrap.split--end > :first-child { flex: 999 1 0%; min-inline-size: var(--_split-min); }
  .split--wrap.split--end > :last-child { flex: 1 1 var(--_split-basis); min-inline-size: auto; }

  /* All in one row, or all stacked, never a mix: each child's basis is
     hugely negative (so it grows from nothing) while the switcher is wider
     than --switcher-at (default 30rem), and hugely positive (so each takes
     a row) once it is narrower. More than four children always stack —
     five equal columns are too narrow to read at any width this product
     ships. A primitive of its own, with its own gap hook. */
  .switcher {
    display: flex;
    flex-wrap: wrap;
    gap: var(--switcher-gap, var(--gap-group));
    --_switch-at: var(--switcher-at, 30rem);
  }
  .switcher > * { flex-grow: 1; flex-basis: calc((var(--_switch-at) - 100%) * 999); }
  .switcher > :nth-last-child(n + 5), .switcher > :nth-last-child(n + 5) ~ * { flex-basis: 100%; }

  /* .grid--auto with a ceiling: as many --grid-min columns as fit, but never
     more than --grid-max (default 4). The second term of max() is the width
     one column has when exactly --grid-max of them share the row, so
     auto-fit can never place one more. Used with .grid, which supplies
     display and the gap: `class="grid grid--capped"`. */
  .grid--capped {
    grid-template-columns: repeat(auto-fit, minmax(max(min(var(--grid-min, 240px), 100%),
      (100% - (var(--grid-max, 4) - 1) * var(--grid-gap, var(--gap-group))) / var(--grid-max, 4)), 1fr));
  }

  /* --- Hooks stop at the element that sets them ---------------------------
     Custom properties inherit, so a nested primitive took its parent's hook
     in place of its own default (three admin stacks did on adoption, the
     2026-09-29 review). Every primitive's children start with every hook at
     `initial` — for an unregistered property, the guaranteed-invalid value,
     so each var() above takes its fallback. A child's own unlayered rule or
     inline style still wins; the primitive itself is never reset. Edge
     cases: design-system/README.md, "The layout primitives". The private
     --_* properties are deliberately NOT reset: they carry the parent's
     hooks to its children (see "The intrinsic primitives" above). */
  :is(.stack, .cluster, .split, .grid, .switcher) > * {
    --stack-gap: initial;
    --cluster-gap: initial;
    --split-gap: initial;
    --split-fixed: initial;
    --split-min: initial;
    --grid-gap: initial;
    --grid-cols: initial;
    --grid-min: initial;
    --grid-max: initial;
    --switcher-at: initial;
    --switcher-gap: initial;
  }
}
