/*
  APP_SPEC.md section 6, and the previous app's visual language, adopted 2026-09-11.

  Ben supplied that app's design brief and one instruction with it: **"I like the
  visual elements only from the other app."** `DESIGN_BRIEF_REVIEW.md` is the argued
  response and this file is what came of it. What was taken, what was fixed first and
  what was left behind are all recorded there; the short version is here because the
  values are here.

  ONE ACCENT, TWO ROLES, AND THE SPLIT IS THE POINT.

    --accent  is the INK accent. It colours text and thin lines.
    --fill    is the FILL accent. It backs solid things: the primary button, an
              active chip. Text on it is --onfill and nothing else.

  The brief called this "the single most important idea to carry across" and then
  said "on the dark theme both roles are the same gold, so the split is invisible."
  An abstraction that is a no-op in the default theme is an abstraction nothing
  checks: a component reaching for the wrong one looks right, ships, and is wrong the
  first time somebody opens the light theme. The brief records that this had already
  happened once, which is why its light theme needed a patch file.

  So `tests/test_app_ui.py` holds a structural check: no rule may set `color` from
  `--fill` or a `background` from `--accent`. Fifteen lines, the same shape as the P4
  check that catches a rules string in the front end, and it is what makes the split
  real rather than described.

  CONTRAST IS MEASURED, NOT ASSERTED.

  The brief's sixth principle is "contrast and legibility first. Both themes must
  stay readable." Computed, its own values did not hold: ten of the fifteen states it
  defines by OPACITY fail WCAG, and `--line`, which is an input's only boundary,
  measures 1.29 against the card behind it in the light theme on a fill that differs
  from that card by 1.04. A light-theme text input was an invisible rectangle.

  Every value below was solved for its threshold rather than chosen and hoped for,
  and `tests/test_app_ui.py` recomputes all of them on every run. Two consequences
  are visible in the names:

  **`--line` and `--edge` are different tokens.** A divider between two rows may be
  quiet; a control's only boundary may not. `--edge` clears 3.0 against every
  surface, `--line` stays quiet, and using the wrong one is now a visible choice
  rather than an oversight.

  **Nothing is disabled by opacity.** `--disabled-ink` clears 4.5 in both themes and
  a disabled control loses its fill and keeps its border, so it reads as unavailable
  structurally. That matters here more than anywhere: the faded plus at a cap is the
  interaction Ben singled out as the best thing the previous app did, and the brief
  documents no disabled state at all.

  WHAT WAS NOT TAKEN. The bottom nav, at 60px plus safe area, costs 10.6% of an
  iPhone SE's height on every screen, against P15, to solve a problem the floated
  Home link already solves for nothing. The dice, the roll sequence and the motion
  section are 37% of the brief and belong to a dice roller. `--warn` does not exist
  here: this app's severities are illegal, incomplete, unchecked and note, and the
  brief's warning amber sits 1.20 from its own gold, so importing it would have made
  a warning and an active control the same visual event.

  The SPACING is this app's, not the brief's. Every margin and padding below was
  measured against P15 across fourteen viewports, and a 1000-point army on an iPhone
  SE has 23px of headroom. Colour, radius, depth and the weight ladder are portable;
  a height budget is not.
*/

:root {
  /* Surfaces, darkest to lightest. */
  --ground: #0f141c;
  --raised: #1a2230;
  --inset: #232d3e;

  /* Two line tokens, and the difference is load-bearing. See the header. */
  --line: #2c3748;
  --edge: #5f779c;

  /* Text, in a ladder. Every step clears 4.5 on every surface above. */
  --ink: #eef2f9;
  --ink-dim: #9fb0c9;
  --ink-faint: #8b94a6;
  --disabled-ink: #8b94a6;

  /* The accent, in its two roles. --accent is ink, --fill is a background, and
     --onfill is the only thing that goes on top of --fill. */
  --accent: #e0b64f;
  --fill: #e0b64f;
  --onfill: #1a1407;

  /*
    **The three stratagem colours, and they are not this app's to choose.**

    Read out of `wahapedia.ru/wh40k11ed/css/main1.css` on 2026-09-13. They are the
    publisher's, they mean something, and the app's job is to reproduce them rather than
    to harmonise them with its own palette: a player who knows a red banner means the
    opponent's turn should see the same red here.

    **Sampling them off a screenshot was tried first and every value was wrong.** The
    rendered card carries a lighter overlay, so a screen grab reads about 20 per cent
    light: `#22796c`, `#096691` and `#bf0d12` against the published `#007360`, `#003d68`
    and `#9e0b10`.

    The SAME in both themes, deliberately. Every other colour here is solved for its
    surface; these are quoted, and a Harbor-mode variant would be this app inventing a
    value the publisher did not print. White clears 4.5 on all three, measured, which is
    what makes quoting them safe.
  */
  --strat-your: #003d68;
  --strat-either: #007360;
  --strat-enemy: #9e0b10;
  --on-strat: #ffffff;

  /* Status. Not the accent, and never the only signal: every finding carries a
     served word as well, because colour alone fails a reader who cannot see the
     difference between #2faa6a and #ed6b62, which is the commonest one there is. */
  --good: #2faa6a;
  --bad: #ed6b62;
  --on-bad: #1a1407;
  --unfinished: #6095e7;

  /* Depth. Opaque panels with a soft shadow and a 1px top highlight: the brief's
     "solid, not flat", and it is right about why. Translucency and blur cost frames
     on a cheap phone and buy nothing a shadow does not. */
  --card-shadow: 0 10px 26px rgba(0, 0, 0, 0.42);
  --card-top: rgba(255, 255, 255, 0.07);

  /*
    **THE TYPE SCALE. Five sizes, three weights, two tracking values.**

    Ben asked what the situation was and the measurement was the answer: 116 font-size
    declarations across FIFTEEN values, six weights including 550 and 650 at ten uses
    each, and nine letter-spacings, four of them on uppercase labels that were meant to
    look alike. Nothing was wrong with any single one; together they meant no two
    screens agreed on what a heading was.

    He chose scale C from three, on 2026-09-16: the five steps below. It is a ratio
    rather than a list, so a new component asks "which step" instead of "what looks
    right here", and `tests/test_app_ui.py` refuses a raw font-size anywhere in this
    file, which is what stops a sixteenth value arriving next month.

    **Every uppercase label is --t-micro / 700 / --track-caps, no exceptions.** That is
    the other half of the change and the one the eye catches: nine selectors carried
    four sizes, four trackings and two weights between them while all nine were saying
    "this is a label". A rules section banner and a composition heading are now the same
    treatment, which is the point rather than a casualty of it.
  */
  --t-display: 1.25rem;   /* 20px. The masthead heading, one per screen. */
  /* 0.8.1, DR2 (Ben, 2026-09-28: "Add both to the scale", then "Grow to 17px"). A big
     figure, and a tile's or band's name; TYPOGRAPHY.md names every use of each. */
  --t-figure: 1.25rem;    /* 20px */
  --t-name-large: 1.0625rem; /* 17px */
  --t-lead: 0.9375rem;    /* 15px. Unit names, the datasheet name, stat values. */
  --t-body: 0.8125rem;    /* 13px. The default: most controls and section headings. */
  --t-small: 0.75rem;     /* 12px. Notes and rules prose under a control. */
  --t-micro: 0.6875rem;   /* 11px. Every uppercase label. */
  --track-caps: 0.08em;
  --track-display: -0.01em;

  --tabbar: 56px;
  /* 16 since 0.8.1 (DR23, Ben 2026-09-28: "16px gutter"), as section 1 says. */
  --gap: 16px;
  --radius: 14px;
  --radius-small: 10px;
  --tap: 44px;
  /* Where the floating Back sits, and how tall it is. One pair of numbers, because
     three rules have to agree about them since 2026-09-21: the control itself, the
     scrim's bottom padding and the box's own height. The roster moves `--back-lift`
     to clear the findings banner and the other two follow without knowing why. */
  --back-lift: calc(var(--tabbar) + env(safe-area-inset-bottom, 0px) + 12px);
  --back-h: 40px;
}

/*
  Harbor, the light theme.

  The fill goes steel-blue and the ink with it. Gold on white reads muddy, which is
  the brief's own finding and the reason the two-role split exists at all: this is
  the theme where `--accent` and `--fill` diverge, and every component that confused
  them shows up here.

  `--onfill` flips to white because the fill is now dark enough to need it. That is
  one token rather than a rule per fill, which is what the brief recommended after
  hard-coding `#1a1407` on each one.
*/
:root[data-theme="light"] {
  --ground: #eaeef4;
  --raised: #ffffff;
  --inset: #f2f5f9;

  --line: #dde3ec;
  --edge: #85888e;

  --ink: #1b232f;
  --ink-dim: #4a586b;
  --ink-faint: #5c6b80;
  --disabled-ink: #5c6b80;

  --accent: #7a5a00;
  --fill: #e0b64f;
  --onfill: #1a1407;
  --on-bad: #ffffff;

  --good: #1c7b4c;
  --bad: #c0392b;
  --unfinished: #2b62c9;

  --card-shadow: 0 8px 22px rgba(30, 45, 70, 0.10);
  --card-top: rgba(0, 0, 0, 0.04);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-text-size-adjust: 100%;
}

#app {
  /* Wider than a reading column on purpose. This is a table of numbers consulted at
     arm's length, not prose, and on a tablet or a phone in landscape the extra width
     is what pays for the height. */
  max-width: 940px;
  margin: 0 auto;
  padding: var(--gap);
  padding-bottom: calc(var(--gap) * 3);
}

.loading { color: var(--ink-dim); }

/* --- the home screen ------------------------------------------------------------- */

/*
  A WELCOME, not a menu, since 2026-09-13. The three destination cards were the tab bar
  drawn twice; what the first screen does now is say what this is and what it stands on.

  Centred, and only here. Every other screen in this app is a left-aligned column of
  numbers you scan, and centring those would cost reading speed for nothing. This screen
  is read once, slowly, and its content is two paragraphs and a list of sources, so the
  page reads as a title page instead of an empty table.

  `max-width` on the prose while the section stays full width: 60ch is a reading measure
  and `#app` is 940px because it holds tables. Without this the two paragraphs run the
  full width of a tablet in landscape and the eye loses the line.
*/
/* ONE rule centres this screen, not two. `justify-items: center` on the masthead grid
   did the same job a second way, and the mutation pass showed what that costs: remove
   either and the screen still looks right, so neither is testable and both are free to
   drift. P2 is written about derivations and the reason applies to layout. */
.welcome { text-align: center; }
.welcome .masthead { margin-top: 8vh; }
/* The front door followed the rest on 2026-09-15: "Apply this masthead styling to all
   mastheads." It was 28px here against 32 elsewhere; it is now the same 22px as every
   other screen, so the app has one heading size rather than three. */
.welcome .masthead h1 { letter-spacing: var(--track-display); }
.welcome .masthead .detachment { margin: 0; }
.welcome .about {
  max-width: 60ch;
  margin: 20px auto 0;
  text-align: left;
  color: var(--ink-dim);
  font-size: var(--t-lead);
}
.welcome .about p { margin: 0 0 10px; }

/*
  The sources, at the bottom, in the same two-column line as every other list in the
  app: name on the left, its figure right-aligned. They are DERIVED from the manifest
  and the loaded stores, so this block is a measurement of what is installed, not a
  claim about it, and it is styled quietly for that reason. It is provenance, not
  a feature list.
*/
.welcome .sources {
  max-width: 60ch;
  margin: 28px auto 0;
  text-align: left;
  border-top: 1px solid var(--line);
  padding-top: 12px;
}
/* A quiet label, not an `h2` at its default 1.5em. This block is provenance and it
   sits under two paragraphs of prose: a heading twice the size of the text it
   introduces makes the sources the loudest thing on the welcome screen. */
.welcome .sources .label {
  margin: 0 0 6px;
  font-size: var(--t-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--ink-faint);
}
.welcome .sources .line .name { font-size: var(--t-body); font-weight: 600; }
.welcome .sources .line .points {
  color: var(--ink-faint);
  font-weight: 400;
  font-size: var(--t-small);
}

/*
  Three destinations, and there are three on purpose. Each one is a whole tap target
  rather than a title with a link in it: at a table, with dice in the other hand, the
  row is the button.

  KEPT after the home screen stopped using them: `Chooser` still draws this list on the
  Dataslate tab, where the two destinations are Rules and Datasheets.
*/
.destinations { list-style: none; margin: 0; padding: 0; }
.destinations li + li { margin-top: 8px; }
.destinations a {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0 10px;
  align-items: baseline;
  min-height: 64px;
  align-content: center;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--raised);
  padding: 10px 14px;
  color: var(--ink);
  text-decoration: none;
}
.destinations a:hover { border-color: var(--ink-faint); }
.destinations .title { font-size: var(--t-lead); font-weight: 700; }
.destinations .count {
  color: var(--ink-faint);
  font-size: var(--t-body);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.destinations .blurb {
  grid-column: 1 / -1;
  margin-top: 2px;
  color: var(--ink-dim);
  font-size: var(--t-body);
}

/*
  Top rather than bottom. Section 6's thumb-reach rule is about the primary action on
  an in-game screen, and navigation is not that: a Home link where the thumb rests is
  how you leave a game by accident.
*/
/*
  FLOATED, SO IT COSTS NO HEIGHT. As its own block it added 44px to every screen and
  pushed a 1000-point list 23px below the fold on Ben's phone, which is the thing P15
  exists to hold. Floated into the masthead it sits beside the title, inside a row that
  is already 47px tall, and the height budget is unchanged.

  `padding` rather than `min-height` for the tap target: padding grows the box a finger
  can hit without growing the line it is floated out of.
*/
.home-link {
  float: right;
  margin-left: 12px;
  padding: 8px 0 8px 8px;
  color: var(--ink-faint);
  font-size: var(--t-small);
  text-decoration: none;
}
.home-link:hover { color: var(--ink-dim); }

/*
  The theme switch, on the home screen only.

  It was floated into the masthead beside the Home link, and the device matrix
  measured the pair at 25px against P15's 21px of headroom on Ben's phone. A theme
  switch is not worth a 1000-point roster falling below the fold, and the home
  screen has the room.
*/
.home .theme { display: flex; gap: 6px; margin-top: 18px; }
.home .theme .chip { min-height: var(--tap); padding: 0 14px; }

/* --- header ---------------------------------------------------------------------- */

/*
  Title and army on the left, the Setup link top right. Ben, 2026-09-12: "a button at
  the very top (small top right) rather than most of the way down the page."

  A grid rather than a float, so a long list name wraps against the button instead of
  running under it: P11 is about a truncated unit name and the same hazard is here.
*/
/*
  **A third of the height it was, since 2026-09-15.** Ben, of the Forces screen: "This
  screen needs to be condensed a bit", then, having seen four sizes: "Go option 3. Apply
  this masthead styling to all mastheads."

  Measured before the change: the header was 91px on Forces, of which 48 was the type and
  43 was the BROWSER'S OWN default h1 margin, 21.44px above and below, which nobody in
  this project ever set. `display: grid` is why it counted: a grid container does not
  collapse its children's margins out of its height the way a block would. So the biggest
  part of the header was an accident.

  22px rather than 32 was the option he picked from four, the others being the same size
  with the stray margins gone, and a 13px caps label. 36px including the margin, against
  103, so it is 67px back on every screen with a masthead.

  **22px became 20 on 2026-09-16**, when Ben chose type scale C and this became the
  scale's display step rather than a number of its own. Nothing about the reasoning
  above changed; what changed is that it is now one of five sizes in the app instead of
  one of fifteen. `--t-display`.

  **And no screen keeps its own size any more.** The unit datasheet used to set 16px
  here, on the argument that a datasheet's name sits over a table and competes with it.
  It sat over a table AND under a popup heading carrying the same four words, which is
  what Ben caught on 2026-09-16: with that heading gone this h1 is the only place the
  unit is named, so it takes the display step like every other screen's does.
*/
.masthead {
  margin: 0 0 10px;
  display: grid;
  gap: 4px;
}
.masthead h1 {
  margin: 0;
  font-size: var(--t-display);
  font-weight: 700;
  letter-spacing: var(--track-display);
  line-height: 1.2;
}

/* STICKY on the roster, not on setup. The total is what you scroll a list to watch,
   the name is what you came to change, and Save was a scroll below the last unit.
   Sticky rather than fixed so it takes its own space in the flow instead of covering
   the first rows; the findings banner is pinned at the other end of the screen and
   the two must not both float over the same content. */
.masthead[data-sticky="true"] {
  position: sticky;
  top: 0;
  z-index: 5;
  /* Out to the page edge and back in, so the banner's ground covers the full width
     while its content stays on the same gutter as everything under it. `#app` sets
     that gutter with `--gap`. */
  margin-inline: calc(var(--gap) * -1);
  padding: 8px var(--gap);
  background: var(--ground);
  border-bottom: 1px solid var(--line);
}
.masthead .title-row,
.masthead .meta-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* The list name: a HEADING, with a pencil beside it. It was a field you typed into
   until 2026-09-13, which is what Ben asked for that morning and reversed the same day.
   The masthead sticks on the roster, so the title sits under your thumb for the whole
   of a game; a field there is one accidental touch from a renamed list. */
.masthead .list-name {
  flex: 1 1 10ch;
  min-width: 0;
  margin: 0;
  font-size: var(--t-lead);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: var(--track-display);
  /* P11's rule, on the one name the person wrote themselves. */
  overflow-wrap: break-word;
}
.masthead .chip.rename { flex: none; }
.rename-form .field-row { margin-bottom: 0; }
.rename-form .actions { margin-top: 14px; }

/*
  **The banner row, D3: what this army IS on the left, what it COSTS on the right.**

  The army line takes the space and the figure takes what it needs, which is the way
  round that keeps the number in the same place on every list: a roster with three
  detachments and one with none both put the total against the right edge, so the eye
  does not have to find it again after every edit.
*/
.masthead .meta-row .figure {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: 4px;
  font-size: var(--t-lead);
  font-weight: 700;
}
.masthead .meta-row .points { font-variant-numeric: tabular-nums; }
.masthead .meta-row .of { color: var(--ink-faint); font-weight: 400; }
.masthead .remaining {
  flex: 1 1 10ch;
  min-width: 0;
  margin: 0;
  font-size: var(--t-small);
  color: var(--ink-dim);
  overflow-wrap: break-word;
}
/* The detachments, quieter than the army. Two facts on one line need a hierarchy or
   they read as one long string.

   `taken`, not `det`: `.det` is already the detachment TOGGLE on the setup screen, a
   bordered chip, and reusing the name put a chip around this line. A class name is a
   selector and a selector is global. */
.masthead .remaining .taken { color: var(--ink-faint); }
/* Non-breaking spaces: a plain space in `content` collapses, which put the
   separator against the detachment name. */
/* A comma since 0.8.1 (N1, DR17 and DR26: commas, never dots). */
.masthead .remaining .taken::before { content: ",\00a0"; }

/* **Army rules, bottom left of the masthead, D2.** Its own row, because it opens a
   document and it was sharing a line with the total: a control that opens a thing to
   read has no business beside a number you scan. */
.masthead .rules-row { display: flex; }
.masthead .rules-row .rules-open { margin-bottom: 0; }

/* **Army Setup and Save together, top right, D1.** One group so they wrap together
   rather than one of them dropping under the title on a narrow phone. */
.masthead .top-actions { display: flex; gap: 6px; flex: none; }

/*
  **The save acknowledgement, D4.** Ben: "a quick flicker or similar to signal the list
  has been saved."

  A save writes to `localStorage` and nothing on screen moves, so the only way to know it
  worked was to go and look. The button itself is the signal rather than a message,
  because a message is one more thing to read and dismiss and the question is only
  whether the thing you pressed did something.

  `prefers-reduced-motion` is honoured with a HOLD rather than nothing: the point is the
  acknowledgement, not the animation, so the button takes the accent for the same 700ms
  and simply does not pulse getting there.
*/
@keyframes saved-flash {
  0%   { background: var(--fill); color: var(--onfill); border-color: var(--fill); }
  70%  { background: var(--fill); color: var(--onfill); border-color: var(--fill); }
  100% { background: transparent; color: var(--ink-dim); border-color: var(--edge); }
}
.chip[data-saved-flash="true"] {
  animation: saved-flash 700ms ease-out 1;
}
@media (prefers-reduced-motion: reduce) {
  .chip[data-saved-flash="true"] {
    animation: none;
    background: var(--fill);
    color: var(--onfill);
    border-color: var(--fill);
  }
}

/* --- verdict --------------------------------------------------------------------- */

/* One line when the answer is LEGAL, because the answer is one word. The explanation
   appears only when there is something to explain, and the findings below carry the
   detail in every case where there is any. */
.verdict {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  border-left: 3px solid var(--line);
  border-radius: 0 6px 6px 0;
  background: var(--raised);
  padding: 7px 10px;
  margin-bottom: 12px;
}
.verdict[data-verdict="ILLEGAL"] { border-left-color: var(--bad); }
.verdict[data-verdict="INCOMPLETE"] { border-left-color: var(--unfinished); }
.verdict[data-verdict="LEGAL"],
.verdict[data-verdict="LEGAL_SUBJECT_TO_UNCHECKED"] { border-left-color: var(--good); }

.verdict .word { font-size: var(--t-lead); font-weight: 700; }
.verdict .why { color: var(--ink-dim); font-size: var(--t-body); }

/* --- the roster ------------------------------------------------------------------ */

h2.section {
  margin: 16px 0 6px;
  font-size: var(--t-body);
  font-weight: 600;
  color: var(--ink-dim);
}

/*
  One column on a phone held upright, two when there is width for two.

  Measured 2026-09-10 across fourteen viewports: a twelve-selection Incursion list runs
  to 779px in a single column and 474px in two, which fits every tablet in both
  orientations and every phone 891 CSS px tall or more held upright. Landscape is where
  the width is going spare, so that is where it gets spent. Width decides rather than a
  media query, because the thing that decides is how much room the list has, not what
  the device calls itself.

  320px is the floor because the longest unit name in the game needs about that much
  before it wraps to three lines, and a third column on a big tablet would buy height
  the screen does not need at the cost of a name breaking badly.

  MULTI-COLUMN RATHER THAN GRID. `grid-template-columns` fills row by row, so a
  two-column army list reads 1, 2 / 3, 4 across the page. An army list has an order and
  a reader follows it down, so `columns` is the correct mechanism: it fills the first
  column to the bottom and then starts the second, and 1 to 6 sits above 7 to 12. This
  was `repeat(auto-fill, minmax(320px, 1fr))` for about an hour and it read wrong on
  the first tablet screenshot.

  `break-inside: avoid` because a row splitting its name from its points across a
  column boundary would be P1 broken by typography.
*/
/* --------------------------------------------------------------------------
   `.label`, defined ONCE. Ben's item 2, option C, 2026-09-22.

   **This class was referred to by 38 scoped rules and defined by none of them**, so a
   screen that wrote its own got the treatment it asked for and a screen that forgot got
   the user agent's. Gameplay forgot, and its four section headings measured 18.72px,
   which is Chrome's default `h3` and is not one of this scale's five steps. That was the
   single largest thing making Gameplay look like a different app.

   The treatment is not new. It is the one `styles.css` already documented at
   `.list-summary h3.label` as "the one treatment every uppercase label in this app
   takes", and which `test_every_uppercase_label_takes_the_same_treatment` has enforced
   across the stylesheet since 2026-09-16. It simply had nowhere to live.

   Low specificity on purpose: every scoped rule below still wins, so a screen that
   deliberately differs keeps differing and only the screens with nothing at all change.
   -------------------------------------------------------------------------- */
.label {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
}
h3.label, h4.label { margin: 14px 0 6px; }

.lines {
  list-style: none;
  margin: 0;
  padding: 0;
  columns: 320px 2;
  column-gap: 20px;
}
.lines > .line { break-inside: avoid; }

.line {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0 10px;
  align-items: baseline;
  padding: 5px 0;
  border-bottom: 1px solid var(--line);
}

/*
  P11: a unit name is never truncated on a screen where the user is choosing between
  units. The previous build showed "Foetid Bloat-drone with H..." on exactly this
  screen. Wrapping is the fix, and it stays the fix at 411 CSS pixels where the longest
  name genuinely needs two lines. Density is bought everywhere except here.
*/
/* **All 23 faction names on `#/factions` measured `rgb(158, 158, 255)` until
   2026-09-22**, which is `#9e9eff`, the user agent's default link colour and a colour in
   neither theme's token set. The rows are `<a class="name">` and nothing said otherwise.
   A link inside this app is a row, and a row takes the app's ink. */
.line .name,
.line > a,
.destinations a { color: var(--ink); text-decoration: none; }

.line .name {
  font-size: var(--t-lead);
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: break-word;
  word-break: normal;
  white-space: normal;
  text-overflow: clip;
}

/* **16px until 2026-09-22**, which is the body's own size and not on the scale at all:
   this rule set four properties and never a size, so every price and every unit count in
   the app sat one step ABOVE the 15px name it belongs to. Found by
   `test_no_text_on_a_dataslate_screen_takes_a_size_off_the_scale`, which asks the browser
   rather than the stylesheet, and not by anybody reading this rule. */
.line .points {
  font-size: var(--t-body);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  text-align: right;
  white-space: nowrap;
}

/* At most one secondary line per row, and only when it says something. */
.line .detail {
  grid-column: 1 / -1;
  color: var(--ink-faint);
  font-size: var(--t-small);
  line-height: 1.35;
  margin-top: 1px;
}

/* The roster total lives in the sticky banner since 2026-09-13, so `.total` is a
   NAME for the number now rather than a block of its own: the layout comes from
   `.meta-row` above and everything here is about how the figure reads.

   `.points` holds the total and nothing else, so a test can read it as a number. The
   limit sits beside it rather than inside it: an element that carries a machine
   readable value should not also carry decoration. */
.total .figure { display: flex; align-items: baseline; gap: 4px; }
.total .points { font-variant-numeric: tabular-nums; }
.total .of { color: var(--ink-faint); font-weight: 400; }

/* --- findings -------------------------------------------------------------------- */

.findings { list-style: none; margin: 0; padding: 0; }

.finding {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--raised);
  padding: 8px 10px;
  margin-bottom: 6px;
}
.finding .head {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: baseline;
  margin-bottom: 3px;
}
/* **Sentence case, and that is now said rather than inherited.** A finding's label is
   the verdict word: "Illegal", "Note", "No datasheets banked". It is not a section
   heading, `P7` gives it a severity colour of its own, and a whole verdict in capitals is
   a shout. It kept sentence case by accident until 2026-09-22, because `.label` was a
   class nothing defined; defining it turned "Note" into "NOTE" and
   `test_the_banner_counts_the_jobs_and_not_the_sentences` said so within the minute. */
.finding .label {
  font-size: var(--t-body);
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
}
.finding[data-severity="illegal"] .label { color: var(--bad); }
.finding[data-severity="incomplete"] .label { color: var(--unfinished); }
.finding[data-severity="unchecked"] .label,
.finding[data-severity="note"] .label { color: var(--ink-dim); }

/* P4: every rules text on screen carries the clause it came from. */
/* **In the accent since 2026-09-13, F9.** Ben: "clause chip orange." It is the way to
   the whole printed rule, and it is the only control in a popout that has now lost the
   sentence that used to point at it: the omitted-parts line went in the same item,
   because the chip IS that pointer and saying so above it was the fact twice. */
.clause {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-micro);
  color: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 4px;
  padding: 0 4px;
}

.finding .message { margin: 0; font-size: var(--t-body); overflow-wrap: break-word; }
.finding .subjects { margin: 4px 0 0; font-size: var(--t-small); color: var(--ink-dim); }

/* --- the Rules surface ------------------------------------------------------------ */

.search {
  width: 100%;
  min-height: var(--tap);
  margin: 4px 0 8px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--raised);
  color: var(--ink);
  font: inherit;
}
.search::placeholder { color: var(--ink-faint); }

/* A section of the book, collapsed. 28 of them open at once is a wall, and the whole
   table of contents on one screen is the thing a printed book gives you for free. */
.section-group {
  border-bottom: 1px solid var(--line);
}
.section-group > summary {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 0 10px;
  align-items: center;
  padding: 10px 0;
}
.section-group > summary .points {
  color: var(--ink-faint);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}
.section-group .lines { padding-bottom: 8px; }

/*
  **THE OPENED SECTION, TIERED. Ben, 2026-09-16: "it reads as a wall of text."**

  It did, and the measurement said why: 38 rows in Core Concepts, every one of them
  15px at weight 600 in `--ink` with the clause id beside it at the same size and
  heavier. Nothing on the screen was more important than anything else, so the eye had
  38 equal things to sort and no way in.

  Three changes, and none of them hides a row.

  **The id stops shouting.** It was as loud as the title it belonged to, which is
  backwards: nobody scans a table of contents by number. It is now `--t-micro`, dim
  and tabular, so the numbers line up in a quiet column you can still read a reference
  off.

  **A child reads as a child.** `01.02.03` is part of `01.02`, and the numbering said
  so all along. Children are indented under their parent behind a hairline, at weight
  400 in `--ink-dim`, so a parent row is the thing you scan and its parts are what you
  find once you have stopped there.

  **The section opens with its own words** instead of a row repeating the banner.

  The two-column flow is OFF once a section is tiered. `.lines` runs `columns: 320px 2`
  on a wide screen, and a nested list inside a balanced column reads as a third
  indentation level that is not one: the child block of `01.02` could break across the
  column gap and continue at the top of the next column under nothing at all.
*/
.section-group .section-opening {
  margin: 0 0 6px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
  font-size: var(--t-small);
  color: var(--ink-faint);
  max-width: 78ch;
}
.section-group > .lines { columns: auto; }
.section-group .lines > .line { padding: 5px 0; }
.section-group .lines .points {
  font-size: var(--t-micro);
  font-weight: 400;
  color: var(--ink-faint);
}
/* **`--t-body` until 2026-09-22**, which made a clause name 13px here and 15px on every
   other list in the app: the same role, two sizes, which is Ben's item 2 in one rule.
   Option C gives the role one size and the SIZE is all that changes here. The depth
   tiering below keeps its weight and its ink, because that carries a fact about the
   clause rather than a difference between screens, and `.line.child` is how a reader
   tells `01.02` from `01.02.03` in a 300-row index. The cost is a longer index, which is
   what option C buys consistency with. */
.section-group .lines .name { font-size: var(--t-lead); }
/* The block of children under one parent. A list item rather than a bare list, because
   it is a child of the `ul` it sits in and an invalid one would be at the mercy of the
   browser's own repair. */
.section-group .lines > .kids {
  list-style: none;
  margin: 0 0 4px 3px;
  padding: 0 0 0 11px;
  border-left: 1px solid var(--line);
}
.section-group .lines > .kids > .lines { padding-bottom: 0; }
.section-group .line.child { border-bottom: 0; padding: 2px 0; }
.section-group .line.child .name {
  font-weight: 400;
  color: var(--ink-dim);
}

/* A clause id, anywhere it appears. Always a link, because a rule almost always
   points at another one and following that should be one tap. */
a.clause { text-decoration: none; }
a.clause:hover { color: var(--ink-dim); }
/* A reference the corpus could not resolve to a clause. Shown as what it says and
   deliberately not tappable: a chip that looks like a link and goes nowhere teaches
   the reader to distrust every chip. */
.clause.dead { border-style: dashed; color: var(--ink-faint); }

.line a.name { color: inherit; text-decoration: none; }

/*
  VERBATIM AND PARAPHRASE ARE NEVER MIXED. APP_SPEC.md 4.3.

  The publisher's text sits on the raised ground with a left rule, unstyled and first.
  Ours sits flat, dimmer, and says so in its own heading. The visual difference is
  load-bearing: a player quoting the paraphrase across a table is the defect these two
  rules exist to prevent, and it is not prevented by the label alone.
*/
.clause-read section { margin: 0 0 14px; }

.clause-read .label {
  margin: 0 0 4px;
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--ink-dim);
}

.verbatim {
  border-left: 3px solid var(--line);
  border-radius: 0 6px 6px 0;
  background: var(--raised);
  padding: 10px 12px;
}

/*
  **THE PRINTED TEXT, AS THE BLOCKS THE PUBLISHER WROTE.**

  Ben, 2026-09-16, on `02.02.01 Modifiers`: "You see how these are just big paragraphs
  with no spaces and no title hierarchy?"

  This was one `<p>` holding the whole clause, and `02.02.01` is stored with 82 newlines
  and 28 blank-line paragraph breaks that HTML collapses to spaces. 207 of the 333
  clauses were drawn that way. The structure was in the corpus the whole time.

  What a block IS is decided in `app/api/blocks.py` and arrives as a `kind`; everything
  below is how each kind looks. The type comes off the scale and adds no step to it: a
  printed heading is the app's one uppercase treatment, which puts a section title inside
  a rule on the same footing as every other label on every other screen.

  **Spacing is the publisher's own.** `data-spaced` says a blank line stood before this
  block in the source, so a new section is set off from the one above it and a run of
  bullets is not. Without it the screen would have to invent its own rhythm, and inventing
  rhythm in printed rules text is how two paragraphs come to look like one.
*/
.verbatim .printed { margin: 0; font-size: var(--t-lead); line-height: 1.5; }
.verbatim .printed + .printed,
.verbatim .printed + ul,
.verbatim ul + .printed { margin-top: 8px; }
.verbatim .printed[data-spaced="true"] { margin-top: 14px; }
.verbatim h3.printed {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-dim);
  margin-top: 18px;
}
.verbatim h3.printed:first-child { margin-top: 0; }

/* A numbered step hangs its number, so a step that wraps stays in one column and the
   sequence reads down the numbers. The indent is the width of "1. " and nothing about
   the number is drawn by this file: it is in the publisher's text. */
.verbatim .printed.step { padding-left: 22px; text-indent: -22px; }
/*
  **A table, drawn as one.** Two clauses hold a table and the corpus holds each as
  pipe-separated lines: `25.03`'s battle sizes, which is the table Ben reads before every
  game, and `19.04`'s attachment durations. Seven rows in the whole book, so this is a
  small thing that happens to sit on the screen a list is started from.

  Which row is the header arrives as `header` from the API. Everything else here is
  layout, and the horizontal scroll is the same trade the weapons tables make: a table of
  five columns of numbers does not wrap into a phone, and a value reconstructed from two
  wrapped lines is worse than one you swipe to.
*/
.verbatim .printed-table {
  border-collapse: collapse;
  margin-top: 10px;
  font-size: var(--t-body);
  font-variant-numeric: tabular-nums;
}
.verbatim .printed-table th,
.verbatim .printed-table td {
  text-align: left;
  padding: 5px 14px 5px 0;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.verbatim .printed-table th {
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  white-space: nowrap;
}
.verbatim .printed-table tr:last-child td { border-bottom: 0; }
.verbatim ul.lines-of-text {
  margin: 8px 0 0;
  padding-left: 18px;
  font-size: var(--t-lead);
  line-height: 1.5;
}
.verbatim ul.lines-of-text li { margin-top: 3px; }
/* The corpus's own second level: 69 lines across 15 clauses sit indented under the line
   above them. Drawn as a nested list rather than as a deeper margin, so the relationship
   is structural for a screen reader too. */
.verbatim ul.lines-of-text[data-depth="1"] { margin-top: 3px; padding-left: 34px; }

.paraphrase p { margin: 0 0 4px; font-size: var(--t-body); color: var(--ink-dim); }
.paraphrase p:last-child { margin-bottom: 0; }
.paraphrase .label { color: var(--ink-faint); }
/* A captured field, which is this project's structuring of the printed text and not a
   quotation of it. The key is set apart so the sentence reads as an answer to it. */
.paraphrase .field .key {
  color: var(--ink-faint);
  font-weight: 600;
  margin-right: 4px;
}

.faqs dl { margin: 0; }
.faqs .faq + .faq { margin-top: 8px; }
.faqs dt { font-size: var(--t-body); font-weight: 600; }
.faqs dd { margin: 2px 0 0; font-size: var(--t-body); color: var(--ink-dim); }
.faqs .source { margin: 6px 0 0; font-size: var(--t-micro); color: var(--ink-faint); }

.sidebars p { margin: 0 0 6px; font-size: var(--t-body); color: var(--ink-dim); }

.when p, .when ul { margin: 0; font-size: var(--t-body); }
.when ul { padding-left: 18px; }

.beginner p { margin: 0; font-size: var(--t-body); color: var(--ink-dim); }

.refs .chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; }
.refs .named { list-style: none; margin: 6px 0 0; padding: 0; font-size: var(--t-body); }
.refs .named li { padding: 0; border-top: 1px solid var(--line); }
.refs .named li:first-child { border-top: 0; }
/*
  **An attached-unit link, item 25, and this reverses the note that used to sit here.**

  It read: "not the accent: one accent, one job, and the primary action on a screen is
  never a list of twelve of them." Ben's mark-up item 25 is "make these look interactive
  (hard to tell)", which is that reasoning failing in the only test that matters. On
  2026-09-18 he also ruled the accent alone carries a control (item 15, the dotted
  underline struck), so a list of ink-coloured anchors with no underline and no row was
  a control wearing the treatment of a plain name.

  The row is the control now: accent, the full width of the block, a hairline between
  rows, and the 34px hit area the `i` and the qualifier already use. The bullets go with
  it, because a bullet is what says "this is a list" and the thing being said is "each of
  these opens".
*/
.refs .named a.explains {
  display: flex;
  align-items: center;
  min-height: 34px;
  color: var(--accent);
  text-decoration: none;
  font-weight: 600;
}
.refs .named a:hover { color: var(--ink); }
/* A reference this corpus cannot resolve. Shown, not hidden: a rule that points at
   something we do not hold is a gap at the point of use, and C3 runs both ways. */
.refs .unresolved li { color: var(--ink-faint); }
.refs .unresolved li::after { content: " (not in the corpus)"; }

/* --- the Datasheets surface -------------------------------------------------------- */

.sheet-read section { margin: 0 0 14px; }
.sheet-read .label {
  margin: 0 0 4px;
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--ink-dim);
}

/*
  **Six characteristics on ONE row, and the invulnerable save under the save.**

  It was a wrapping flex row of equal cells. That is fine at six and wrong at seven: the
  seventh cell stretched the full width on its own, and on Ben's phone the sixth, OC,
  wrapped down with it. His words, 2026-09-16: "ideally we're fitting all 6 on the first
  row and invuln save (when applicable) sits neatly below save."

  A grid of exactly as many columns as there are characteristics cannot wrap at any
  width, which removes the failure rather than tuning it. **Neither number is written
  here.** `--stat-columns` is the length of the served `stats` array and `--stat-column`
  is the index of whichever characteristic the API says an invulnerable save belongs
  under, both set by `units.js` from the payload. A stylesheet that said `repeat(6, 1fr)`
  and `grid-column: 3` would be this file holding two facts about the characteristic
  line, and those belong to `views.py`.

  A stat line that needs a sideways swipe is the one thing on this screen a player looks
  at every turn, so the cells are narrow and the row is not scrollable: at 320px, six
  cells of 1fr in a 292px column is 48px each, which fits "10\"" and "2+" with room.
*/
.statline {
  display: grid;
  grid-template-columns: repeat(var(--stat-columns, 6), 1fr);
  gap: 4px;
  margin: 0 0 12px;
}
.statline .stat {
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--raised);
  padding: 5px 2px;
  text-align: center;
}
.statline dt { font-size: var(--t-micro); color: var(--ink-faint); font-weight: 600; }
.statline dd {
  margin: 1px 0 0;
  font-size: var(--t-lead);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* The invulnerable save is a characteristic the printed sheet puts in its own box,
   and it is the one D1 is about, so it is marked rather than blended in. It falls into
   the second row, in the column the API named, so it reads as a save rather than as a
   seventh characteristic. */
.statline .invsv {
  border-color: var(--unfinished);
  grid-column: var(--stat-column, auto);
}

/*
  A weapons table is the one thing on these screens allowed to scroll sideways. Seven
  columns of numbers do not wrap into a phone, and a stat you have to reconstruct from
  two wrapped lines is worse than one you swipe to.

  **The NAME wraps instead, since 0.7.5.** Ben, 2026-09-27: *"in some of the pop up
  datasheets i have to scroll to the right to see damage for weapon characteristics. Can
  we make the weapon name return across two lines if needed to make room for the full
  weapon profile?"* The argument above is kept for the numbers, which still never wrap;
  what it did not weigh is that a name read over two lines costs nothing, and D pushed off
  the edge of a pop-up is a stat you have to go looking for. So the table has no minimum
  width and the name column no minimum, and the characteristics take their fixed widths
  first. The scroller stays for a screen too narrow even for the seven numbers.
*/
.scroller { overflow-x: auto; }

/*
  **The two weapon tables line up with each other, F2.** Ben: "match the weapons tables:
  contents centred, and the columns aligned between ranged and melee."

  They are two separate `table` elements, one per heading, so `width: 100%` alone gave
  each one column widths sized to its own contents: a melee table with no Range values
  and shorter names put every characteristic in a different place from the ranged table
  above it, and the eye has to find the column again on each.

  `table-layout: fixed` with a width per column is what makes two tables one grid. The
  name column takes the space and the seven characteristics are equal, which is what the
  printed card does; `ch` rather than a percentage so the numbers do not reflow as the
  screen narrows, and the `.scroller` handles the overflow it causes on a small phone.

  Contents CENTRED, which is the other half of the item and the reason the alignment
  reads at all: right-aligned single characters sat against the next column's left edge,
  so two adjacent columns looked like one. The name stays left, because it is a name.
*/
.weapons table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-body);
  table-layout: fixed;
  /* No minimum width since 0.7.5 (was 22rem): the name column wraps instead. */
  min-width: 0;
}
.weapons th, .weapons td {
  padding: 4px 2px;
  text-align: center;
  white-space: nowrap;
  width: 3.5ch;
  /* Every row's text centred on its row, 0.7.5 (Ben: "Make sure each rows text is
     centred vertically"): a name on two lines no longer leaves its numbers at the top. */
  vertical-align: middle;
}
/* Range is the one characteristic that is not one or two characters: `24"` in the body
   and `Range` in the head. Sized for the heading, so it does not clip into the column
   beside it, which is what `RangA` was. */
.weapons th:nth-child(2), .weapons td:nth-child(2) { width: 5.5ch; }
/* And A, which can carry `D6+3` (0.9.5: Mortarion's Rotwind ran into the BS column). */
.weapons th:nth-child(3), .weapons td:nth-child(3) { width: 5.5ch; }
/* And D, which carries `D6` and `D3` as often as a digit. */
.weapons th:last-child, .weapons td:last-child { width: 5.5ch; }
.weapons th[scope="row"], .weapons thead th:first-child {
  width: auto;
  text-align: left;
  white-space: normal;
  padding-left: 0;
}
.weapons th[scope="row"] {
  /* 150px until 0.7.5, which is what pushed D off a pop-up's edge. */
  min-width: 0;
  overflow-wrap: break-word;
  font-weight: 600;
}
.weapons thead th { color: var(--ink-faint); font-size: var(--t-micro); font-weight: 600; }
.weapons tbody tr { border-top: 1px solid var(--line); }
.weapons td { font-variant-numeric: tabular-nums; }

.wkeywords {
  display: block;
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 400;
}
.wnotes { margin: 4px 0 0; font-size: var(--t-small); color: var(--ink-faint); }

.ability, .option, .quoted { margin: 0 0 8px; }

/* What a filtered datasheet dropped, and what it could not account for. C5a: an
   instrument's exclusions are part of its answer, so this is not a footnote to be
   made quiet, it is the sentence that makes the two-row weapon table trustworthy. */
.filtered {
  border-left: 3px solid var(--line);
  border-radius: 0 6px 6px 0;
  background: var(--raised);
  padding: 6px 10px;
  margin: 0 0 10px;
}
.filtered .field { margin: 0; font-size: var(--t-body); }
.filtered .message { margin: 3px 0 0; color: var(--ink-dim); font-size: var(--t-small); }

/*
  The datasheet in the Builder, which is a popup and no longer an inline block.

  It carried `margin-top`, a `border-top` and `padding-top` from the day it expanded
  inside a build row: "inset so it reads as belonging to the line above it rather than
  as the page having changed." There is no line above it any more, and with the popup's
  heading dropped on 2026-09-16 that rule was drawing a divider across an otherwise
  empty strip between the close X and the unit's name. Separation is the box's job now.

  Its `h1` override went with it, for the reason recorded at `.masthead`.
*/
.unit-sheet { grid-column: 1 / -1; }

/* What the faction's ARMY rule does, and what a detachment does. Both are BODIES of
   a box since 2026-09-13 rather than accordions in a section of their own: the army
   rule opens from a button under the army selector, and a detachment's rules open
   from the `i` on its own row. Ben: "the rules should be easily accessible at the
   point of making the decision rather than scrolling to a separate place."

   So there is no disclosure furniture left to style, only the spacing between the
   blocks inside a box that is already open. */
.army-rules { margin: 0; }
.army-rules > [data-army-rule],
.taken-detachment {
  padding-bottom: 10px;
  margin-bottom: 10px;
  border-bottom: 1px solid var(--line);
}
.army-rules > [data-army-rule]:last-child,
.taken-detachment:last-child { border-bottom: 0; margin-bottom: 0; }
.army-rules h4.label,
[data-detachment-rules] h4.label { margin: 8px 0 4px; }
[data-detachment-rules] > div + div { margin-top: 8px; }

/* The control that opens the army rule. Named rather than an `i`, because an `i`
   qualifies something on screen and there is nothing here to qualify: this is a
   document you are choosing to open. */
.rules-open { margin-bottom: 12px; }

/* One detachment per row since 2026-09-13. A wrapped row of chips packed nine into
   four lines and made the name the only target; a column gives each a full-width
   target with its cost in a constant place, and room for the `i` that opens it. */
.det-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; }
.det-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.det {
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--tap);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: transparent;
  color: var(--ink-dim);
  font: inherit;
  font-size: var(--t-body);
  text-align: left;
  padding: 0 10px;
  cursor: pointer;
}
/* **Selected takes the accent since 2026-09-18**, Ben's mark-up item 39: "make these
   an orange border when selected instead." `--good` was the green this app uses for a
   verdict, and a chosen detachment is not a verdict about anything. The accent means a
   thing you can touch or the state you are in, which is what this is. */
.det.on { border-color: var(--accent); color: var(--ink); }
/* Disabled by STRUCTURE, not by opacity, same as the primary button: the border
   goes quiet and the text takes `--disabled-ink`, which clears 4.5 in both themes.
   `opacity` would put this row's name below the line and call that a state. */
.det[data-blocked="true"] {
  border-color: var(--line);
  color: var(--disabled-ink);
  cursor: not-allowed;
}
.det[data-blocked="true"] .det-dp { color: var(--disabled-ink); }
.det-name { min-width: 0; overflow-wrap: break-word; }
.det-dp { font-variant-numeric: tabular-nums; color: var(--ink-faint); flex: none; }
.det.on .det-dp { color: var(--ink-dim); }
/* The refusal the DP counter cannot explain: a Unique tag refusing a PAIR. Full
   width under its own row, because it is about that row and nothing else. */
.det-blocked {
  flex: 1 0 100%;
  margin: 0 0 2px;
  font-size: var(--t-small);
  color: var(--ink-faint);
}
.saved { margin-top: 14px; }
.saved .lines { columns: 1; }
.saved .line .name.add { text-align: left; }

/* The publisher's own flavour text. Roman since 2026-09-16: italic is now the app
   speaking, and this is quoted. It is already set apart by being dim and by sitting
   above the rule it introduces. */
.army-rules .flavour,
[data-detachment-rules] .flavour {
  margin: 0 0 3px;
  color: var(--ink-faint);
  font-size: var(--t-small);
}
.ability .who, .quoted .who {
  margin: 0;
  font-size: var(--t-body);
  font-weight: 600;
}
.abilities .message, .wargear .message, .quoted .message {
  margin: 1px 0 0;
  font-size: var(--t-body);
  color: var(--ink-dim);
}
.wargear .option ul, .composition ul {
  margin: 3px 0 0;
  padding-left: 18px;
  font-size: var(--t-body);
  color: var(--ink-dim);
}
.composition .field, .abilities .field { margin: 3px 0 0; font-size: var(--t-body); }
.composition .field .key, .abilities .field .key {
  color: var(--ink-faint);
  font-weight: 600;
  margin-right: 4px;
}

/* **In the accent since 2026-09-13, F9.** Ben: "keyword orange". A keyword is the one
   thing on a datasheet that other rules refer to by name, so it is what you scan the
   sheet for; in `--ink-dim` inside a `--line` box it was the quietest thing on it. */
.keyword {
  border: 1px solid var(--accent);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: var(--t-micro);
  color: var(--accent);
}

/*
  **The faction keyword bar, item 22, 2026-09-19.**

  Same chip, different edge. The accent is this app's "tappable" signal since the dotted
  underline came off on 2026-09-18, and a faction keyword opens nothing: the corpus
  explains what a keyword IS and has no clause for which army list a unit belongs to. A
  chip in the accent that does nothing when pressed is the promise `Explain` refuses to
  make on the 972 unit keywords the corpus cannot explain either, and it should not be
  made here for the sake of matching.

  `--edge` is the line token that is readable on every surface in both themes, which the
  stylesheet test asserts, so the chip still reads as a chip.
*/
.keyword.faction {
  border-color: var(--edge);
  color: var(--ink);
}

[data-role="faction-keywords"] { margin-top: 10px; }

/* --- the Builder ------------------------------------------------------------------- */

/*
  A roster row while it is being edited. The name and points sit where they sit on the
  roster screen, so a list reads the same whether you are building it or looking at it,
  and the controls take the third column rather than pushing the numbers around.
*/
/*
  **One row, one line, since 2026-09-12.** It carried up to seven controls across a
  wrapped second row: model count, enhancement, Warlord, attachment, Datasheet, Remove
  and Loadout. On a ten-unit list the unit names were lost inside a field of chips.

  Now the NAME is the control that opens the datasheet, a pencil opens the rest, and
  the only thing on the right is the one destructive action. `auto` on the name column
  would let a long name push the price off; `1fr` makes the name the thing that wraps,
  which is what P11 wants.
*/
/* **Configure sits beside the name since 2026-09-13**, so the columns changed with
   it. The name no longer takes all the slack; it and its Configure button are the
   left group, a spacer pushes the points and the delete to the right, and the two
   controls you use together are no longer at opposite ends of the row with the
   destructive one weighted like the common one. `minmax(0, auto)` so a long name
   still shrinks and wraps rather than pushing the points off the row: P11. */
.build-row {
  /* **Six columns since 2026-09-18**, Ben's mark-up item 51: "move 'X models' to in
     between the unit name and the 'Configure' button so it's a single rod (where
     possible)". The strength had a full-width row of its own under the name, so a
     five-unit list was five rows of name and five rows holding one small select. It is
     now column 2, and the row it used to occupy is gone. */
  grid-template-columns: minmax(0, auto) auto auto 1fr auto auto;
  align-items: center;
  column-gap: 8px;
  padding: 8px 0;
}
/* An attached character reads under the unit it leads, indented, with a rule down
   the left so the bundle is one object at a glance. Ben: "bundled characters and
   units are in one place the same as they would be on the table." */
.build-row.attached {
  padding-left: 12px;
  border-left: 2px solid var(--line);
  margin-left: 2px;
}
.build-row > .name {
  grid-column: 1;
  grid-row: 1;
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-size: var(--t-lead);
  font-weight: 600;
  /* **Bold white, and nothing else**, Ben 2026-09-18 after seeing the accent version:
     "i'm ok to remove [the underline]. i think the bold lettering in white will carry it
     enough. I want to play with it a bit before making further calls."

     So a unit name on the roster signals by weight and by being the brightest ink in its
     row, which is the same job the underline did and the 2026-09-13 reasoning kept:
     a roster of ten accent-coloured names is a screen with no hierarchy left. The
     accent stays on the inline rules words, where one word inside a sentence has to be
     findable in it.

     **Named as provisional**, because he said so: he wants to use it before ruling. */
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  overflow-wrap: break-word;
}
/*
  The pencil, "little" as asked. A word rather than a glyph, for the reason the tab bar
  has no icons: this app owns no icon set and an invented glyph reads as nothing to a
  screen reader.

  **Drawn at 26px and TAPPED at 44**, which is the same trick the loadout qualifier
  uses: a control small enough to sit beside a name without becoming the loudest thing
  on the row, with the finger-size floor met by an invisible box around it rather than
  by making the box itself big. Drawn at 44 it read as a second title.
*/
.build-row > .edit {
  grid-column: 3;
  grid-row: 1;
  position: relative;
  min-height: 26px;
  padding: 0 8px;
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: transparent;
  color: var(--ink-dim);
  font: inherit;
  font-size: var(--t-small);
  cursor: pointer;
}
.build-row > .edit::after { content: ""; position: absolute; inset: -9px; }
.build-row > .points { grid-column: 5; grid-row: 1; }
/* The delete. Red, small, far right, and it opens a confirmation rather than acting:
   an X that deletes on the first tap next to a control you press all the time is a
   list you lose a unit from without noticing. */
.build-row > .kill {
  grid-column: 6;
  grid-row: 1;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  min-height: 22px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--bad);
  font: inherit;
  font-size: var(--t-lead);
  line-height: 1;
  cursor: pointer;
}
.build-row > .kill::after { content: ""; position: absolute; inset: -11px; }
/* Which unit is the Warlord, now that the button saying so is behind the pencil. */
.build-row > .tag {
  grid-column: 1 / -1;
  grid-row: 2;
  justify-self: start;
  margin-top: 2px;
  padding: 0 6px;
  border: 1px solid var(--good);
  border-radius: 999px;
  color: var(--good);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
}
/* **In the row rather than under it**, item 51. `where possible` is the whole of the
   qualifier: the select is only drawn where the MFM prices more than one strength, so a
   single-strength unit still has a two-control row and nothing moved for it. */
.build-row > .strength { grid-column: 2; grid-row: 1; justify-self: start; }
.build-row > .detail { grid-column: 1 / -1; grid-row: 3; }
/* What this unit is carrying, under its name. Ben's mark-up item 52, and "small" is his
   word: it is a label for the rows already indented under this one, not a second list. */
.build-row > .attached-note {
  grid-row: 2;
  font-size: var(--t-small);
  color: var(--ink-dim);
}

/* The pencil's box: the four controls Ben named, stacked. */
.unit-options { display: flex; flex-direction: column; gap: 10px; align-items: stretch; }
.unit-options > .chip { min-height: var(--tap); }
.unit-options .field-row { margin-bottom: 0; }
/* The delete confirmation's two buttons, the safe one first. */
.dialog-body .actions { display: flex; gap: 10px; margin-top: 14px; }
.dialog-body .actions > button { flex: 1; min-height: var(--tap); }
.primary.danger { background: var(--bad); border-color: var(--bad); }

/* A small toggle or action. Never the accent: one accent, one job, and the primary
   action on this screen is adding a unit. */
.chip {
  min-height: 32px;
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: transparent;
  color: var(--ink-dim);
  font: inherit;
  font-size: var(--t-small);
  padding: 0 10px;
  cursor: pointer;
}
/* Same change, same reason, for the theme toggle Ben marked up beside it, item 31:
   "make the selection box around it orange." */
.chip.on { border-color: var(--accent); color: var(--ink); }
.chip.remove { color: var(--ink-faint); }
.chip.remove:hover { color: var(--bad); border-color: var(--bad); }

.build-row .strength,
.unit-options .strength,
.unit-options .enhancement,
.unit-options .attach,
.setup select,
.setup input[type="text"] {
  min-height: 32px;
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: var(--raised);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-body);
  padding: 0 6px;
}
/*
  **Dropdowns, G2: "they feel chunky."**

  They were `var(--tap)` tall, full width, with a `--raised` fill and an `--edge` border,
  which is four kinds of weight at once on a screen that is mostly dropdowns. What
  actually makes a select feel heavy is the FILL: a filled box reads as a thing sitting
  on the page, and an outlined one reads as a field in it.

  So the fill goes, the height comes down to 38px, and the border stays at `--edge`
  because that is the control's only boundary and `test_a_control_boundary_is_visible`
  holds it at 3.0 against every surface.

  **38 and not 44, deliberately.** `--tap` is this app's rule for a control you press at
  a table with dice in the other hand; these are on the setup screen and in the options
  box, which are the screens you use BEFORE a game, sitting down. 38px still clears
  WCAG 2.5.8's 24px with room, and the row it sits in is taller than the control.
*/
.setup select, .setup input[type="text"],
.unit-options select {
  min-height: 38px;
  width: 100%;
  background: transparent;
}

/* Where a control used to be, and the sentence that replaced it. The Warlord button
   and the enhancement picker are ABSENT on a unit the rules refuse, not disabled: a
   disabled control invites a press and teaches nothing, and the clause beside the
   sentence is a link to the rule that said no. */
.build-row .why {
  display: block;
  grid-column: 1 / -1;
  margin-top: 2px;
  color: var(--ink-dim);
  font-size: var(--t-small);
}

.setup { margin-top: 18px; }
.field-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 3px;
  margin-bottom: 10px;
}
.field-row > .key {
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--ink-dim);
}
.field-row .chips { display: flex; flex-wrap: wrap; gap: 5px; }

/* While the document and its answer disagree, which is one round trip. Dimmed rather
   than blanked: a screen that flashed empty on every edit would be worse than one
   that shows the last consistent pair a moment longer. */
/*
  The one opacity in this file, and it earns it.

  It marks the whole screen for the length of one round trip, 1.6ms of rules work
  plus a LAN, and it is not a STATE a reader has to read: nothing here is
  permanently at 72%, and the text measures 8.01 in dark and 6.25 in light while it
  is. Every other faded thing in this stylesheet was a control a person had to read
  and could not, which is why they are tokens now.
*/
.builder[data-busy="true"] { opacity: 0.72; }

/* --- save and share ------------------------------------------------------------- */

.share { margin-top: 18px; }
.share .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.share .chip:disabled {
  background: transparent;
  border-color: var(--line);
  color: var(--disabled-ink);
  cursor: not-allowed;
}
.share input[type="text"] {
  min-height: var(--tap);
  width: 100%;
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: var(--raised);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-body);
  padding: 0 8px;
}
/* The share code itself. Monospace and selectable, because the thing a person does
   with it is select all and paste it into a message. */
.packed {
  width: 100%;
  margin-bottom: 8px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--raised);
  color: var(--ink-dim);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-micro);
  padding: 6px 8px;
  resize: vertical;
  word-break: break-all;
}

/* --- the add panel ------------------------------------------------------------- */

/* Inside the picker dialog since 2026-09-12, so it needs no frame of its own: the
   box is the frame. The search sits at the top because on a 103-candidate list it is
   the control, not a refinement. */
.add-panel .search { width: 100%; margin-bottom: 10px; }

/*
  The app's own Back control, above every screen that has one above it.

  **Not sticky, and that is a trade rather than an oversight.** The Builder's masthead is
  already sticky and pinning a second strip above it would mean the masthead's offset
  chasing this strip's height, which is two numbers that can disagree. Scrolled down a
  long roster the phone's own gesture is the control to hand, and since 2026-09-14 that
  gesture is reliable, so what this adds is a control you can SEE on the screens you
  arrive at rather than a second way to leave a list you are deep inside.
*/
/*
  The list name is the control that renames it, F5. A button that has to look exactly
  like the heading it sits in: no fill, no border, the heading's own type, and the hit
  area a phone needs.
*/
.list-name .name-open {
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left;
  min-height: 34px; cursor: pointer;
}

/*
  **Back floats above the bar since 2026-09-15**, and the bar is only ever the three
  sections. It was in the row for a few hours; Ben: "It feels out of place and breaks up
  the rhythm of the three menus accessible from the navigation bar." He chose this from
  five placements.

  Fixed at the bottom left, which is where a right-handed thumb rests, and 12px clear of
  the bar so the two never read as one control.

  **It sits OVER the scrim since 2026-09-21**, items 11 and 23, and that reverses the
  note that used to be here: "`z-index: 19` puts it under the scrim at 50, so a dialog
  covers it the way it covered the old strip". Ben narrowed his request for pop-ups with
  their own Back to *"we can use the same back button on the existing navigation menu"*,
  and the same button is no use if a box draws over it.

  What the old note listed as the four exits was true of a browser tab and not of a
  phone: the X, Escape, the backdrop and Back, where "Back" meant the DEVICE's. Run as a
  standalone app there is no device Back on screen, so with a box open the only exits
  were the X and a tap on the backdrop. Measured 2026-09-21: `elementFromPoint` at the
  control's centre returned the scrim at every depth.

  The tab bar is NOT lifted with it. A box is modal and a tab is a way out of the screen
  underneath; Back is a way out of the box, which is the distinction Ben's wording makes.

  **It floats, so it costs no layout anywhere**, and the one thing it can cover is the
  findings banner, which is fixed at the same corner on the roster screen. The rule below
  lifts it over that banner rather than letting the two share a corner.
*/
.back-float {
  position: fixed;
  left: var(--gap);
  bottom: var(--back-lift);
  z-index: 60;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  padding: 0 14px 0 11px;
  border: 1px solid var(--edge);
  border-radius: 999px;
  background: var(--inset);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-body);
  font-weight: 600;
  cursor: pointer;
  /* Against a card it is sitting on, rather than decoration: without it the pill and a
     tinted saved-list card are two surfaces of nearly the same value. */
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
}

/*
  Above the findings banner when the roster has one. The banner's own row is
  `min-height: var(--tap)` plus its 1px border, which is what this clears; a test measures
  the two boxes on a roster screen and fails if they overlap, so the arithmetic cannot
  drift away from the banner it is about.

  **It moves `--back-lift` rather than the control**, since 2026-09-21. A box now has to
  clear this control, so two rules have to agree about where it is, and the roster is the
  screen where it is NOT in its usual place. Measured while getting that wrong: the
  reservation was written against the plain offset of 68px while the roster's float sits
  at 113px, so the box still landed 29px over the control on the one screen the whole
  thing was being tested on.
*/
body:has(.findings-banner) {
  --back-lift: calc(var(--tabbar) + env(safe-area-inset-bottom, 0px)
                    + var(--tap) + 1px + 12px);
}


/* Three columns in the picker: name, how many are already in the army, and price.
   The count sits BESIDE THE NAME since 2026-09-13, not out on the right beside the
   price, because two numbers in one column that mean different things is a row you
   have to read twice. The base `.line` grid is two, so this widens it. */
/*
  **EVERY cost on the right, D8.**

  It was `minmax(0, auto) auto 1fr` with the three children placed implicitly, and the
  middle child is optional: a unit already in the list pushed its price into the 1fr
  column and got it right-aligned, and a unit not in the list had its price land in the
  auto column immediately after the name. So the prices formed a ragged line that
  followed the length of each unit's name, with only the selected ones on the right,
  which is Ben's item almost word for word.

  Fixed by PLACING the two optional cells rather than letting them fall where they may.
  The TEMPLATE is unchanged, and that matters: the name column is still `auto` so the
  count badge sits immediately after the name, which is Ben's item from earlier the same
  day ("can you put the qty already in army immediately after the unit name instead of
  on the right?"). The two items pull in opposite directions and both are satisfiable,
  because they are about different numbers: the count belongs to the NAME and the price
  belongs to the row. Only the placement was wrong.
*/
/* **The grid moved onto the button on 2026-09-18**, Ben's mark-up item 54: "make the
   whole unit banner clickable to add". The row is now one control with the name, the
   in-army count and the price laid out inside it, so every part of the banner is the
   target rather than just the word. */
.candidate { display: block; padding: 0; }
.candidate-add {
  display: grid;
  grid-template-columns: minmax(0, auto) auto 1fr;
  align-items: center;
  column-gap: 8px;
  width: 100%;
  min-height: var(--tap);
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  padding: 8px 0;
  cursor: pointer;
}
/*
  **A refused row is a ROW, item 57, 2026-09-19.** It takes `.candidate`'s
  `display: block` and has no `button` child to carry the grid, so its name, its `i` and
  its price flowed inline and the `i` wrapped underneath. Flex puts them on one line with
  the price against the right edge, which is where the price sits on every other row in
  this app.
*/
.candidate.refused {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 0;
}
.candidate.refused > .points { margin-left: auto; }

.candidate .in-army { grid-column: 2; }
.candidate .points { grid-column: 3; justify-self: end; }
.candidate .detail { grid-column: 1 / -1; }

/*
  **The count: tighter and in the accent, D10.** It was a 22px pill in `--ink-dim` beside
  the name, which read as another piece of furniture on a row that already has two
  numbers. Tight and coloured, it reads as a state of the row rather than a label on it,
  and it matches the price beside it, which turns the same colour for the same reason.
*/
/*
  **Centred by flex, not by line-height.** This pill, the findings count and the
  armies primary all pushed their text to the middle of a box by setting
  `line-height` to the box's height, in pixels. It works exactly as long as the text
  is one line: the moment it wraps, the second line sits 17px below the first whatever
  size the type is, and the pill grows a gap it never asked for. Flex centring does
  the same job, survives a wrap, and does not have to be edited when the type scale
  moves, which is what made these three visible in the first place.
*/
.candidate .in-army {
  min-width: 0;
  padding: 0 5px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  color: var(--accent);
  font-size: var(--t-micro);
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 17px;
  text-align: center;
}

/* The price of what you have taken, not of one. D9, and the colour is the signal that
   the number changed meaning. */
.candidate .points.spent { color: var(--accent); }

/* The name inside that control. No longer a button of its own, per item 54; it keeps
   the weight and the size, because the name is still the thing being read. */
.candidate .name.add {
  color: var(--ink);
  font-size: var(--t-lead);
  font-weight: 600;
  text-align: left;
  overflow-wrap: break-word;
}
.candidate-add:hover .name.add { color: var(--accent); }

/*
  A refused unit, inside the box that lists what this list cannot take.

  Struck through and compact: the sentence that refused it is behind its own `i` since
  2026-09-13, because one rule applied 22 times printed 22 times is a wall. The group
  it sits in names the clause once.
*/
.refused-group + .refused-group { margin-top: 12px; }
.refused-group > .who { margin: 0 0 2px; }
.candidate.refused .name { color: var(--ink-faint); text-decoration: line-through; }
.candidate.refused .points { color: var(--ink-faint); font-weight: 400; }
.candidate.refused .detail { color: var(--ink-faint); }

/*
  A weapon keyword this unit's ROSTER gave it, rather than one the card prints.

  Marked because the difference matters at a table: the reader has the printed card in
  front of them and a row that silently disagrees with it is the app asserting something
  it cannot point at.

  **THE MARKER IS IN THE TEXT SINCE 2026-09-16, and it is no longer a colour.** It was
  a dashed underline, then blue on Ben's call, and it is now an asterisk on the bracket:
  `[LETHAL HITS]*`, with one line at the foot of the weapon tables saying what an
  asterisk is. Ben, 2026-09-16: "can we change the keywords back to [X] then maybe [X]*
  for added modifiers, noting the additions via the ability below?"

  **Why this is better than the hue it replaces.** The blue was a second meaning on a
  second colour, on a screen where orange had just been narrowed to one meaning, and it
  could only be read by someone who knew what blue meant here. An asterisk is a
  convention every reader already has, it survives a monochrome print and a colour-blind
  reader with no fallback needed, and the thing that explains it is on the same screen
  rather than in this stylesheet. `--granted` went with it: a token nothing sets is a
  meaning nothing can check.

  The keyword is still a control and still underlined like every other one, and the box
  it opens still names the unit and the ability that granted it.
*/
/* The asterisk, beside the keyword and outside its control. `--accent` would make it
   read as another thing to touch; it is a footnote mark, so it takes the ink the row
   already uses and earns its visibility from the note at the foot of the tables. */
.explain > .added { color: var(--ink-dim); }

/* That note. One per sheet, italic because it is the app speaking, and absent when
   nothing on the sheet is marked. */
.added-note {
  margin: 6px 0 0;
  font-size: var(--t-small);
  color: var(--ink-faint);
}

/* Who granted it, above the rule, inside the box the keyword opens. */
.granted-by {
  margin-bottom: 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
}
.granted-by .who { margin: 0 0 4px; font-size: var(--t-body); font-weight: 700; }

/*
  The abilities read and NOT applied. Same treatment as `.filtered`, and for the same
  reason: an instrument's exclusions are part of its answer.
*/
.grant-notes {
  margin-top: 12px;
  padding: 10px 12px;
  background: var(--inset);
  border-left: 2px solid var(--edge);
  border-radius: 0 var(--radius-small) var(--radius-small) 0;
}
.grant-notes > div + div { margin-top: 10px; }
.grant-notes .who {
  margin: 0 0 4px;
  font-size: var(--t-small);
  font-weight: 700;
  color: var(--ink-dim);
}
.grant-notes .quoted { margin: 0; font-size: var(--t-body); }

/* --- provenance ------------------------------------------------------------------ */

/* Collapsed. It is how the answer was reached, which matters when a number looks
   wrong and is noise on every other occasion. Kept one tap away rather than removed,
   because a figure with no traceable source is the thing this project exists to
   prevent. */
.provenance {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  font-size: var(--t-small);
}
.provenance summary { cursor: pointer; min-height: 24px; }
.provenance p { margin: 6px 0 0; overflow-wrap: break-word; }

/* --- the one primary action ------------------------------------------------------ */

.actions { margin-top: 16px; }

/* The Army setup link sits under the primary button rather than beside it. At 320px
   a full-width primary and a chip on one row leaves the chip 60px wide, and "Army
   setup" does not fit in 60px. */
.actions [data-role="to-setup"] { margin-top: 8px; }

/* The detachment picker. Chips rather than a select, because 25.04 allows more than
   one and a multi-select on a phone is a scroll trap. A blocked chip is present and
   unpressable, with the validator's own sentence under the row: the same shape as a
   loadout control at its cap, and the same reason. */
.setup .detachments .chips { margin-bottom: 4px; }

.setup .detachments .chip[data-blocked="true"] {
  background: transparent;
  border-color: var(--line);
  color: var(--disabled-ink);
  cursor: not-allowed;
}

.setup .field-row > .why {
  display: block;
  margin-top: 4px;
  font-size: var(--t-small);
  color: var(--ink-dim);
}

/* The one strongly coloured control on a screen, and therefore a FILL: --fill
   backs it and --onfill is the only thing that goes on top. In the dark theme the
   two accent tokens are the same gold and this looks arbitrary; in Harbor it is the
   difference between readable and not. */
/* `.primary`, not `button.primary`. The New list control on Forces is an ANCHOR,
   because it goes somewhere, and it was drawing as bare link text: the fill lived on a
   `button` selector it could not match, so the one primary action on that screen was
   the least prominent thing on it. Found on screen, not by a test, which is what
   screenshots are for. */
.primary {
  min-height: var(--tap);
  width: 100%;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--fill);
  color: var(--onfill);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

/* Disabled by STRUCTURE, not by opacity. It loses its fill and keeps its edge, so
   it reads as a control that is there and cannot be used, and its text still clears
   4.5. `opacity: 0.5` put it at 2.3 and called that a state. */
button.primary:disabled {
  background: transparent;
  border-color: var(--edge);
  color: var(--disabled-ink);
  cursor: not-allowed;
}

.error {
  border: 1px solid var(--bad);
  border-radius: var(--radius);
  padding: 12px 14px;
  color: var(--ink);
}

/* --- the loadout picker ------------------------------------------------------------
   Ben's previous app is the model: a stepper per weapon and the plus fading out at
   the cap, rather than offering a choice and refusing it afterwards. A disabled
   control is a served answer, never something this stylesheet decided. */

.loadout {
  margin: 8px 0 2px;
  padding: 8px 0 2px;
  border-top: 1px solid var(--line);
}

.loadout-group { margin-bottom: 10px; }

/* A homebrew dossier's Options heading (54.9) is the same label as a group's, so it
   adds no style of its own. */
.loadout-group h4, .loadout .loadout-heading {
  margin: 0 0 4px;
  font-size: var(--t-micro);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-dim);
  font-weight: 700;
}

/* What the models are still carrying. Counted down only by the picks that replace
   that weapon, which is the row the previous app got wrong. */
.loadout .carrying { list-style: none; margin: 0 0 6px; padding: 0; }

.loadout .carrying li {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 8px;
  padding: 1px 0;
  font-size: var(--t-body);
}

.loadout .carrying .held { color: var(--ink-dim); font-variant-numeric: tabular-nums; }

/* Only reachable on a roster built somewhere else: the picker cannot overdraw a
   weapon any more. Its own line rather than a third column, because at 320px a
   sentence beside a name and a number is a name broken over three lines. */
.loadout .carrying .overdrawn {
  flex-basis: 100%;
  color: var(--bad);
  font-size: var(--t-small);
}

.loadout .choice { margin: 0 0 8px; }

/*
  **`.loadout .choice .rule` is gone, and it was dead.**

  It styled the datasheet's printed sentence above the controls, which is a real thing
  and a good place for it. The sentence moved behind the `i` on 2026-09-13 when Ben
  asked for clarifications to go under a tap, and nothing in `app/web` has rendered a
  `.rule` inside a choice since. Two rules survived the move because nobody was looking
  for them, and they were found by the type-scale pass giving each one a new size.

  `D1`: a rule that cannot match is a rule nothing can check. The sentence is still
  there, in the box the `i` opens, set as a `.message` like every other quoted rule.
*/

.loadout .steppers { list-style: none; margin: 0; padding: 0; }

.loadout .steppers li {
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
}

/* The name and its information symbol are ONE grid cell, which is what "next to the
   weapon name" means and also what keeps the row on one line: the template names four
   columns, so a fifth child wrapped the plus onto a row of its own. */
.loadout .steppers .item {
  font-size: var(--t-body);
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
.loadout .steppers .count { min-width: 1.4em; text-align: center; font-variant-numeric: tabular-nums; }

/* **What an option does to the points, F14.** Inside the label cell, not a column of
   its own: the grid above names four and a fifth child wraps the plus onto its own row.
   Dimmed and tabular, because it is a footnote to the weapon's name rather than the
   count on the right, and the two numbers on this row must not read as the same kind of
   thing. Printed only where the option moves the price, which is 174 items of 508. */
.loadout .option-points {
  margin-left: 4px;
  font-size: var(--t-body);
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* 32px of tap target from padding rather than height, so the row stays short: P15
   has 21px of headroom on Ben's phone and a picker is inside a collapsed panel. */
.loadout .step {
  min-width: 30px;
  padding: 5px 8px;
  font: inherit;
  line-height: 1;
  color: var(--ink);
  background: var(--raised);
  border: 1px solid var(--edge);
  border-radius: 4px;
}

/* The fade. A control at its cap is visibly spent rather than missing, because a
   button that vanishes reads as an app fault and a faded one reads as a limit. */
/*
  THE FADED PLUS, which is the interaction Ben named as the best thing the previous
  app did: "the selections would max out and the plus symbols would fade away when
  caps were reached."

  Faded, not invisible, and not `opacity: 0.32`. That measured 1.93 against the
  panel, which is a control you cannot read, and the brief that recommended it
  documents no disabled state at all. This keeps the glyph legible at 4.54 and drops
  the fill and the border, so the difference from a live control is structural and
  survives a reader who cannot tell two greys apart.
*/
.loadout .step:disabled {
  background: transparent;
  border-color: transparent;
  color: var(--disabled-ink);
  cursor: not-allowed;
}

.loadout .choice select { width: 100%; min-height: 38px; background: transparent; }


.loadout .why {
  margin: 2px 0 0;
  font-size: var(--t-small);
  color: var(--ink-dim);
}

.loadout .why[data-role="blocked"] { color: var(--bad); }

.loadout-partial,
.loadout-unread,
.loadout-none,
.loadout-refused {
  margin: 2px 0;
  font-size: var(--t-small);
  color: var(--ink-dim);
}

.loadout-unread .why { display: block; }

/* One note per line under a control. A rider that contradicts the stepper above it
   and a count of what is taken are different sentences, and running them together
   behind a separator made the reader do the sorting. */
.loadout .why .bit { display: block; }
.loadout .why .bit + .bit { margin-top: 1px; }

/* A sub-list this strength does not reach. Dimmer than a refusal and quieter than an
   offer, because it is neither: nothing is wrong and nothing can be done here until
   the unit changes size. The `i` beside it opens the datasheet's own condition. */
.loadout-withheld {
  margin: 4px 0 2px;
  padding-left: 10px;
  border-left: 2px solid var(--line);
  font-size: var(--t-small);
  color: var(--ink-faint);
}


/*
  **ROMAN IS THE PUBLISHER TALKING, ITALIC IS THE APP TALKING.**

  Ben, 2026-09-16: "For the small 12px text, can we introduce italics?" He then chose
  B over A, which is the difference between italic as decoration and italic as a
  meaning. A set the whole 12px tier in italic and swept the datasheet's own sentence
  up with it; B sets only the app's own asides, and leaves anything quoted upright.

  This project draws one line harder than any other, and it is the line `A2` is about:
  the publisher's words are quoted and this app's words are its own. That line was
  carried by labels and by position and by nothing you could see at a glance. It is
  now carried by the letterforms, at no cost and with no new token.

  **What this took OFF italic, and it is a real change rather than a tidy-up.** The
  army rule's flavour text, a detachment's flavour, and the publisher's Designer's
  Note were all italic, which under this rule now reads as the app speaking when all
  three are quoted. The Designer's Note keeps its separation structurally instead,
  with the rule down its left edge, because F9 set it apart deliberately and the
  reason for that has not changed: it is the publisher clarifying the rule, not part
  of the rule.

  Every selector below is the app's own voice: a count it derived, a limit it is
  enforcing, something it filtered, something it cannot offer. None of them is printed
  on anything.

  **What is NOT here is the part that took the work.** The list under a loadout control
  is a mixture: "1 of 2" is this app counting two served numbers, and "This model's
  boltgun cannot be replaced" is a rider the datasheet prints. They arrive in one array
  and were rendered by one line of code, so italicising `.loadout .why` set the
  publisher's sentence in the app's voice, which is exactly the version Ben rejected.
  `Why` in `build.js` now marks each bit with whose it is and only `ours` is italic.

  `.loadout-unread` is out for the same reason: what it prints IS the datasheet's
  sentence, the one this app could not parse, and it is the last place that should read
  as the app talking. `.loadout-partial` prints a model's name and what it carries,
  which is retrieved rather than said by anyone.
*/
.filtered .message,
.sheet-head .filtered,
.det-blocked,
.loadout .why .bit[data-voice="ours"],
.loadout .carrying .overdrawn,
.loadout-withheld,
.loadout-none,
.loadout-refused,
.added-note,
/* The line introducing a datasheet's own override of `19.01`. This app naming what the
   sentence beneath it does, so it takes the app's voice; the sentence stays roman,
   because it is the publisher's. `GAPS.md` section 11, Batch A. */
.attach-permitted .who {
  font-style: italic;
}

/* That override, under the attach control. Set as a quoted rule like every other
   retrieved sentence in this app, and boxed off by the same left rule the designer's
   note uses, so it reads as a thing the datasheet says rather than as a warning. */
.attach-permitted {
  margin: 4px 0 0;
  padding-left: 8px;
  border-left: 1px solid var(--line);
}



/*
  **A body of rules text from the DATASHEET stores, as blocks.** The same shape as the
  Rules screen's printed text and the same reason, but a different cause: there the
  structure was in the corpus and the screen collapsed it, here it was destroyed at
  capture by `probe.text` and recovered by `probe.prose`. See `ui.js`'s `Prose`.

  Scoped to the message classes rather than to a container, because these blocks appear
  inside five different boxes: an army rule, a detachment rule, an enhancement, a
  stratagem's effect and a unit's ability.
*/
.printed-heading {
  margin: 12px 0 4px;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-dim);
}
.printed-heading:first-child { margin-top: 0; }
.message + .message,
.message + ul.lines-of-text,
ul.lines-of-text + .message { margin-top: 8px; }
.message[data-spaced="true"] { margin-top: 12px; }
.message.step { padding-left: 22px; text-indent: -22px; }
ul.lines-of-text {
  margin: 6px 0 0;
  padding-left: 18px;
  font-size: var(--t-body);
}
ul.lines-of-text li { margin-top: 3px; }
ul.lines-of-text[data-depth="1"] { margin-top: 3px; padding-left: 34px; }

/* --- depth, and the ring that says where you are ---------------------------------- */

/*
  The brief's signature, and the reason it reads as solid rather than flat: an
  opaque panel with a soft shadow and a 1px top highlight. No glass, no blur. Both
  are free of layout cost, which is why they could be taken whole when the spacing
  could not.
*/
.destinations a,
.verdict,
.add-panel,
.filtered,
.finding {
  box-shadow: var(--card-shadow), inset 0 1px 0 var(--card-top);
}

/*
  FOCUS, which the brief does not mention at all.

  A tablet with a keyboard is a real way to use this, and a visible focus ring is
  the difference between a keyboard being usable and being a guess. `:focus-visible`
  rather than `:focus`, so a mouse press does not leave a ring behind it.

  The accent, because a ring is a thin line and that is what --accent is for. It
  measures 7.24 in dark and 7.83 in light against every surface.
*/
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

/*
  A NAME NEVER TRUNCATES, in either theme and at any width. P11, and it is listed
  here with the other states because the brief documents none of them and this is
  the one a player notices: "Foetid Bloat-drone with H..." on the screen where you
  choose between units.
*/
.line .name,
.candidate .name,
.saved .line .name {
  overflow-wrap: break-word;
  text-overflow: clip;
}

/* --- the tab bar, and the shell that makes room for it ---------------------------

   Ben chose the official app's bottom banner over his previous app's Back, Home and
   Settings on 2026-09-11, and over P15, the invariant that a 1000-point army plus its
   detachment fits one screen. So this bar is allowed to cost height, which the Home
   link it replaced was not.

   Fixed rather than sticky, and the shell pads the main column by the bar's own
   height plus the phone's safe area, so nothing is ever underneath it. `100dvh` not
   `100vh`: on a phone `vh` is the tallest the viewport ever gets, so a bar sized
   against it sits below the address bar until you scroll. */
.app-shell {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
.app-main {
  flex: 1;
  padding-bottom: calc(var(--tabbar) + env(safe-area-inset-bottom, 0px) + 8px);
}
.tabbar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 20;
  display: flex;
  height: calc(var(--tabbar) + env(safe-area-inset-bottom, 0px));
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--raised);
  border-top: 1px solid var(--line);
}
.tabbar .tab {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  color: var(--ink-dim);
  font-size: var(--t-body);
  font-weight: 600;
  letter-spacing: var(--track-caps);
  /* The whole tab is the target, so the tap area is the column and not the words. */
  min-height: var(--tap);
  position: relative;
}
.tabbar .tab.on { color: var(--accent); }
/* The active tab also carries a rule, because colour alone is not a signal to a
   reader who cannot separate gold from grey. WCAG 1.4.1. */
.tabbar .tab.on::before {
  content: "";
  position: absolute;
  top: 0;
  width: 34%;
  border-top: 2px solid var(--accent);
}

/* Forces' landing, and Settings. Both borrow the home screen's column. */
/*
  **Two buttons on one line, since 2026-09-15.** Ben: "New army and import side by side
  yes please." They are both short words and they were a stacked 94px under a screen whose
  whole complaint was height.
*/
.armies .actions {
  margin: 12px 0 4px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
.armies .actions .primary {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  text-align: center;
  text-decoration: none;
}
.setting { margin-top: 14px; }
.setting .label { margin-bottom: 6px; }

/*
  **Item 29, 2026-09-19: "title font heirarchy here feels a bit off?"**

  It was. The screen had `h1` at `--t-display` and every section heading at `--t-micro`,
  the uppercase label treatment, including the `h3` nested inside About: a five-step scale
  used as two steps with a 0.56rem cliff between them, and a section indistinguishable
  from the label inside it.

  Three steps now, and a section heading is a SENTENCE rather than an uppercase label.
  That is what frees it to take its own size: every uppercase label in this stylesheet
  shares one treatment by test, so the only way to give a section its own step is to stop
  making it a label. The `h3` inside About keeps the label treatment, which is correct —
  it names a list of sources rather than a section of the screen.
*/
.settings-screen .section-title {
  margin: 0 0 6px;
  font-size: var(--t-lead);
  font-weight: 600;
}

/*
  The label INSIDE a section, which names the sources list under About. It had no rule
  anywhere: `.label` is scoped per screen in this stylesheet and Settings had no scope of
  its own, so this `h3` took the browser's default 18.72px. Measured on screen before the
  fix, the three headings were 20px, 11px, then 18.72px — large, tiny, medium — which is
  what Ben was looking at when he said the hierarchy felt off.

  It takes the uppercase label treatment, which is what it is: it names a list, not a
  section of the screen.
*/
.settings-screen .setting h3.label {
  margin: 10px 0 4px;
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
}

/*
  **Item 30: "MAKE THIS TEXT FIELD A LITTLE BIT BIGGER AND WHITE."**

  It was a bare `<input>` with no rule against it anywhere, so it took the browser's
  default size and the browser's grey. This is the treatment the Builder's own setup
  fields already have, applied rather than invented: two rules for one kind of control is
  how two screens come to disagree about what a text field looks like.
*/
.settings-screen input[type="text"] {
  min-height: 38px;
  width: 100%;
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: var(--t-body);
  padding: 0 8px;
}

/*
  **A saved list is a CARD of two lines, since 2026-09-15.** Ben, having seen the screen
  on his phone: "This screen needs to be condensed a bit", and then the layout in his own
  words: "1st line: name on left, edit and delete on right. 2nd line: faction spread
  across the entire bar. Whole card colour coded?"

  Measured before the change: the row was 102px carrying about 44px of content. The grid
  was `auto 1fr auto auto` and came out 97 + 172 + 45 + 45 of 383 available, so Delete had
  nowhere to go and wrapped under the tag, which is where 58 of those 102 pixels went. The
  card is 79px and it holds one more control than the row did.

  **The unit count is gone**, at his instruction: "You don't need to show how many units
  are in the army - just the faction and name."

  **Tinted, not filled**, which was the option he chose from four mocked up on 2026-09-15.
  The card carries the faction colour at 22 per cent over the app's own raised surface and
  the second line carries it at full strength with white on it. The list NAME therefore
  sits on a surface this app controls: a filled card would put it on whatever lightness the
  publisher happened to choose, and those range from `#347379` to `#4b6262`, so the same
  screen would have shouted at one army and whispered at another.
*/
.armies .lines { columns: 1; }
.saved-list {
  display: block;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--raised);
  margin-bottom: 8px;
}
.saved-list .card-top {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px 8px 12px;
  min-height: var(--tap);
}

/*
  The faction, across the whole card, in the publisher's colour.

  **It was a fixed box to the left of the name until 2026-09-15**, and the box is what
  made every row tall: 14ch by 3.3rem, sized so that `AGENTS OF THE IMPERIUM` at 22
  characters matched `ORKS` at four. Consistent, and large. A full-width bar is consistent
  for free, because every card is the same width, and it costs one line of small type.

  Still not truncated: a keyword too long for the bar wraps and is still whole, which is
  `P11` one column to the left of where `P11` is written. At 411px the bar holds about 44
  characters, so nothing in the store reaches a second line.
*/
.faction-tag {
  display: block;
  padding: 5px 12px;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
  border-top: 1px solid var(--line);
  overflow-wrap: break-word;
}

/*
  The publisher's own banner colour for the page, served per army. The card takes it at 22
  per cent and the bar at full, with white on the bar: white clears WCAG AA on all 23
  banked values, measured, and black fails on 21.

  A faction whose colour is not banked keeps the plain card and the dim label above rather
  than being given a colour this app invented, which is the two factions with no Wahapedia
  page at all.
*/
.saved-list[style*="--faction-colour"] {
  background:
    linear-gradient(0deg,
      color-mix(in srgb, var(--faction-colour) 22%, transparent),
      color-mix(in srgb, var(--faction-colour) 22%, transparent)),
    var(--raised);
  border-color: color-mix(in srgb, var(--faction-colour) 55%, var(--line));
}
.saved-list[style*="--faction-colour"] .faction-tag {
  background: var(--faction-colour);
  border-top-color: var(--faction-colour);
  color: #fff;
}

/* The name is the control that opens the summary, so it is a button that reads as a
   title. Same treatment as `.line a.name`: the row's own type, no chrome. */
.saved-list .name {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--t-lead);
  font-weight: 600;
  line-height: 1.3;
  text-align: left;
  color: var(--ink);
  cursor: pointer;
  overflow-wrap: break-word;
}
.saved-list .name:hover { color: var(--accent); }
.saved-list .name { flex: 1; min-width: 0; }
.saved-list a.chip { display: inline-flex; align-items: center; text-decoration: none; }
/*
  **36px rather than 32, since 2026-09-15.** These were the two smallest controls on the
  screen and Delete now sits beside Edit, so the pair is one mis-tap wide. Still under the
  44px the tab bar is held to, because two 44px chips and a name do not fit 383px without
  the name wrapping; 36 is what the row affords.

  **Delete is the quieter of the two**, deliberately. It takes the plain line rather than
  the edge, so the destructive control is not the one that catches the eye, and it asks
  before it does anything.
*/
.saved-list .chip { min-height: 36px; }
/* A favourite Combat Patrol, 2026-09-25: the name opens the patrol, so it is a link that
   reads as the same title, and the badge says what kind of army the card is. */
.saved-list a.name { text-decoration: none; }
.saved-list .badge {
  flex: none;
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
}
.saved-list .chip.remove { border-color: var(--line); color: var(--ink-faint); }

/*
  What a saved list is, at a glance. No rules in here, which is Ben's instruction and
  also the right line: the rules are two taps away in the builder and a copy here would
  be a third place they have to agree.
*/
/* A GRID, not an inline-block with a `min-width`. The first build set the key to 9ch
   and "DETACHMENTS" is eleven characters, so the longest label on the screen ran into
   its own value: "DETACHMENTSPlague Company". A grid column sizes itself to the widest
   key there actually is. */
.list-summary .field {
  margin: 0 0 4px;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0 12px;
  align-items: baseline;
  font-size: var(--t-body);
}
.list-summary .field .key {
  color: var(--ink-faint);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  font-weight: 700;
}
.list-summary .field .value { overflow-wrap: break-word; }
.list-summary .label { margin: 14px 0 4px; }
.list-summary .lines { columns: 1; }
.list-summary .line .points { display: flex; gap: 4px; align-items: baseline; }
.list-summary .line .points .unit {
  font-weight: 400;
  color: var(--ink-faint);
  font-size: var(--t-small);
}

/* Delete sits at its own size on the right. `.dialog-body .actions > button` gives
   every button `flex: 1`, which is right for a confirm-and-cancel pair and wrong for a
   lone destructive control: it stretched Delete across the full width of the box, which
   is the emphasis a primary action gets. */
.list-summary .actions.secondary {
  margin-top: 16px;
  justify-content: flex-end;
}
.list-summary .actions.secondary > button { flex: 0 0 auto; }

/*
  The QR, since 0.9.0, where the labelled placeholder stood (it was drawn so the encoder
  would land in a space already seen). Centred, 2026-09-26 (Ben: "centre the QR code
  rather than left justified"). White with black modules in both themes: a reader looks
  for dark on light, and the SVG carries its own four-module quiet zone.
*/
.qr-frame {
  width: 100%;
  max-width: 340px;
  margin: 0 auto 8px;
  border-radius: var(--radius-small);
  overflow: hidden;
  background: #fff;
}
.qr-frame .qr { display: block; width: 100%; height: auto; }
.qr-note { text-align: center; margin: 0 auto 12px; max-width: 34em; }
.share-in { display: grid; gap: 8px; margin: 0 0 12px; }

/* --- a rules name that opens its rule ---------------------------------------------

   Ben's item 22. The summary has to read as tappable without becoming a button: it is
   inline, inside a weapon's keyword line and a core-ability row, so a border or a fill
   would break the line it sits in. It was a dotted underline in the accent until
   2026-09-18; it is now the accent alone, per his mark-up item 15. See below. */
.explain { display: inline; }
/*
  ONE gesture for "this opens something", shared rather than copied.

  Ben chose the dotted underline from four on 2026-09-13 for the roster's unit names
  (D6), and a tappable rules word already had it, so one rule carried both.

  **The underline came off on 2026-09-18**, his mark-up item 15: "remove the dotted
  underline completely from all areas. the orange colour is enough of a prompt to press."

  That is a replacement rather than a deletion and it is worth saying which. What signals
  "this opens something" is now the accent on the text, which is the rule set on
  2026-09-16 when the accent stopped meaning anything else: orange is a thing you can
  touch or the state you are in, and nothing else in this app is orange. The block that
  used to argue a border or a fill was refused here still holds, for the same reason it
  always did: these sit inside a weapon's keyword row and a chip per keyword turns a
  two-line table cell into six.

  **What that costs, named.** Colour is now doing the work alone on these controls. The
  hover rule that turned the underline solid has gone with it, so a pointer gets no
  change of state; on a phone, which is where this app is used, there was never a hover
  to lose.
*/
.explains {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
  color: var(--accent);
}
.explain > .explains {
  display: inline;
  color: var(--accent);
}
.explained { display: inline; }

/* The keyword block's one control sits on its own line under the chips rather than
   inside them, because it explains the block and is not one of them. */
.refs .explain { display: block; margin-top: 8px; }

/* --- the overlay ------------------------------------------------------------------

   Ben, 2026-09-12: "a pop up (overlay box that has a X in top right to close it) rather
   than a banner style opening".

   `position: fixed` and a flex centre, not a transform: a transform-centred box whose
   content is taller than the screen cannot be scrolled to its top, and a rule with a
   Designer's Note on it is taller than a phone in landscape. The box caps at the
   viewport and scrolls inside itself instead, with the head pinned so the X is always
   under the thumb.

   The scrim sits above the tab bar, which is the only other fixed element, so the bar
   cannot be tapped through an open dialog. */
/*
  **The box has to clear the tab bar. Ben, F8, 2026-09-14:** "You can't scroll pop up
  windows down far enough. The bottom is cut off by our navigation bar."

  Measured on a 411x891 viewport before the fix: the dialog's bottom edge sat at 875,
  the tab bar's top at 835, and the last element in the body ended at 860 with the body
  already scrolled to its end. So the final 25px of every long popup lived inside the
  bar's band, and no amount of scrolling reached it.

  The scrim is `z-index: 50` and the bar is `20`, so the box DRAWS over the bar and the
  content is legible in a screenshot. That is what made this easy to miss and irrelevant
  in use: on the phone the bar is where your thumb is, and content under it is content
  you cannot touch or comfortably read.

  So the bottom padding clears the bar and the safe area under it, and the box's height
  is computed from the same expression. `padding-block` rather than a `padding`
  shorthand, so the 16px side gutter is stated once and the top and bottom vary.
*/
/* **A dialog is never a label, whatever it is nested inside.**

   `Note` and `explain` render their dialog as a DOM child of the control that opens it,
   and that control is often an `h3.label`. `text-transform` and `letter-spacing` are
   inherited, so the moment `.label` acquired `uppercase` on 2026-09-22 the whole of
   clause `02.05` was shouted at a reader in the publisher's own words. That is `A2`
   broken by a typography rule, which is the quietest way to break it.

   Reset at the overlay boundary rather than on the one dialog that showed it: the
   nesting is how every dialog in this app is drawn, so the next one would have inherited
   it too. Caught by `test_a_unit_keyword_is_not_a_button_and_the_block_says_why`, which
   reads the rule's text and is the only test that would have. */
.scrim,
.dialog,
.dialog-body { text-transform: none; letter-spacing: normal; }

.scrim {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  padding-bottom: calc(var(--tabbar) + env(safe-area-inset-bottom, 0px) + 16px);
  background: rgba(0, 0, 0, 0.55);
}
/*
  **And the band grows again where Back is drawn**, 2026-09-21. `F8` widened the bottom
  padding so a box clears the tab bar; Back now sits inside that cleared band, 12px above
  the bar and 40px tall, so the same defect returns one control lower unless the box
  moves up by the control's height and a gap.

  Keyed on `data-back`, which `BackButton` sets only where `parentOf` finds a screen
  above this one. A tab root draws no Back and pays nothing for one.
*/
:root[data-back] .scrim {
  padding-bottom: calc(var(--back-lift) + var(--back-h) + 12px);
}
/*
  **And the box's own height, which is computed from the same expression and had to move
  with it.** `F8`'s note says so in as many words: the padding positions the box and
  `max-height` sizes it, and changing one without the other leaves a box that is placed
  correctly and still tall enough to reach under the control. Measured 2026-09-21: with
  the padding alone the gap was -55px, because the box was 803px tall in a band 751px
  high.
*/
:root[data-back] .dialog {
  max-height: calc(100dvh - var(--back-lift) - var(--back-h) - 12px - 16px);
}
.dialog {
  width: 100%;
  max-width: 32rem;
  max-height: calc(100dvh - var(--tabbar) - env(safe-area-inset-bottom, 0px) - 32px);
  display: flex;
  flex-direction: column;
  background: var(--raised);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.4);
}
.dialog:focus { outline: none; }
.dialog-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
  padding: 12px 14px 8px;
  border-bottom: 1px solid var(--line);
}
/* A head with no heading is a close control and nothing else, so it loses its rule and
   most of its height: the body underneath opens with its own h1 and draws its own. */
.dialog-head[data-titled="false"] {
  padding: 6px 14px 0;
  border-bottom: 0;
  justify-content: flex-end;
}
/*
  **A POPUP TITLE IS NOT ORANGE, AND THAT REVERSES G3.**

  G3 was Ben's, on 2026-09-13: "accent colour in the loadout screen title, and in popup
  titles generally." It shipped as one rule here and one on the loadout key below.

  He took it back on 2026-09-16, having looked at the whole palette at once: "I'm ok
  with it being both interactive AND current state. No use for the title being orange."
  That is the sharper rule and it is why this reversal is an improvement rather than a
  wobble. Orange now means exactly two things, and both of them are things you can act
  on: you can touch this, or this is the state you are in. A title is neither. While
  titles were orange as well, every orange thing on the screen had to be read before
  you knew whether it was a control, which is the cost that only shows up once there
  are enough of them.

  What replaced it: the box already has a border, a shadow, and a heading in the
  brightest ink on the screen. It was never the colour doing that work.
*/
.dialog-head .section { margin: 0; }
/* 44px, the same finger target as a tab. An X that needs aiming is an X a person
   taps three times. */
/*
  **The close X: smaller and orange, G1.** Ben named it four times across the four
  documents, which is the most any item in this review was asked for.

  It was a `.chip`, so it carried a border and read as a boxed control the size of a
  button on a row that already has the box's title on it. **The TARGET is unchanged at
  44px** and that is the whole trick: the glyph shrinks and the hit area does not, which
  is the same technique the loadout qualifier uses. A close control on a phone is the one
  thing you press without looking.
*/
.dialog-head .close {
  flex: none;
  min-width: 44px;
  min-height: 44px;
  margin: -8px -10px 0 0;
  border-color: transparent;
  background: transparent;
  color: var(--accent);
  font-size: var(--t-lead);
  line-height: 1;
}
.dialog-head .close:hover { color: var(--ink); }
.dialog-body {
  overflow-y: auto;
  padding: 12px 14px 14px;
}
.dialog-body .message { margin: 0 0 10px; white-space: pre-wrap; }
.dialog-body .wnotes { margin: 0 0 10px; }
.dialog-body .chips { margin: 0; }

/* --- a group heading is not a unit name -------------------------------------------

   Ben, 2026-09-11: "I think we need a font colour change or something to help
   differentiate main headings (Character etc.) from units."

   Colour AND shape, not colour alone. Both reference apps set a group heading in
   capitals with letter spacing against sentence case on the rows, which changes the
   silhouette of the word, so it still reads as a heading for someone who cannot
   separate the two hues. WCAG 1.4.1. */
.lines-group > h3.section,
h3.section[data-role-heading] {
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  margin: 16px 0 4px;
}

/* --- a cross-reference is one button, title first ---------------------------------

   Ben's item 18. The id stays, quieter and below, because it is how a clause is
   named in conversation and in every other citation this app prints. A chip that was
   only an id could not say where it went; a chip that is only a title cannot be
   quoted. */
.pointer {
  display: inline-flex;
  flex-direction: column;
  gap: 1px;
  padding: 6px 10px;
  min-height: var(--tap);
  justify-content: center;
  border: 1px solid var(--edge);
  border-radius: var(--radius-small);
  background: var(--inset);
  text-decoration: none;
  color: var(--ink);
}
.pointer .title { font-size: var(--t-body); font-weight: 600; }
.pointer .id {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-micro);
  color: var(--ink-faint);
}
/* Unresolved: named, listed, and not a link. Dotted rather than merely dimmer, so it
   does not read as a button that failed to load. */
.pointer.dead {
  border-style: dashed;
  color: var(--ink-dim);
  background: none;
}

/*
  **A fold's header reads as a header, since 2026-09-15.** Ben, of the rules screen:
  "Make the collapsible banner headers visually distinct from its contents. Reads as a
  single wall, all the same font."

  It did. A `summary` renders as body text with a triangle, so a page of folds and their
  contents was one column of one size, and the only thing saying which line you could
  press was the marker the browser draws. The treatment here is the same on both folds
  the rules screen has, the section list and the provenance, because two different header
  styles on one screen is the wall again in two fonts.

  What makes it a header and not a line: smaller type in caps with tracking, the ink at
  full strength against dimmed contents, a rule under it, and a chevron on the right that
  turns when it opens. The caps are the thing that carries it at 13px on a phone.

  `list-style: none` on both the element and the WebKit pseudo-element, because Safari
  and Chrome disagree about which one draws the marker and leaving either in puts a
  triangle beside the chevron.
*/
.section-group > summary,
.aside > summary {
  list-style: none;
  cursor: pointer;
  min-height: var(--tap);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink);
}
.section-group > summary::-webkit-details-marker,
.aside > summary::-webkit-details-marker { display: none; }
.section-group > summary::after,
.aside > summary::after {
  content: "";
  width: 7px;
  height: 7px;
  border-right: 2px solid var(--ink-faint);
  border-bottom: 2px solid var(--ink-faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 120ms ease;
}
.section-group[open] > summary::after,
.aside[open] > summary::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}

/* The one fold on a rule page. Everything Ben asked to stop seeing lives behind it. */
.aside { margin-top: 14px; border-top: 1px solid var(--line); }
.aside > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 0;
}
.aside > .provenance { color: var(--ink-dim); font-size: var(--t-body); }
/* The clause id on the closed header, so the answer to "where from" is legible without
   opening anything and the fold holds the detail rather than the fact. */
.aside > summary .title { flex: 1; }
.aside > summary .badge {
  font-weight: 600;
  letter-spacing: var(--track-caps);
  color: var(--ink-faint);
}

/* --- the rules index: a section is not a rule -------------------------------------

   Ben: "change the font of rules sections to be less similar to the header/banner",
   and "we don't need this sub text". The sub-line went; this is the other half. */
/*
  The title inherits the header treatment above since 2026-09-15 rather than setting its
  own. It carried `font-size: 0.9375rem; letter-spacing: 0; text-transform: none` to make
  a section look less like the masthead, which was Ben's instruction on 2026-09-12: "change
  the font of rules sections to be less similar to the header/banner". Both readings are
  his and the later one wins; what it costs is that a section header and the app's banner
  are now nearer each other in weight, and what it buys is that a section header and the
  clause rows under it are not.
*/
.section-group > .section-head .title { min-width: 0; }

/* --- the loadout picker's qualifier ------------------------------------------------

   Ben's item 30. Floated, so it costs no height in a list that already has six of
   them, and finger-sized, because a control that costs no height still has to be
   hittable. Open, it drops the datasheet's sentence below the control it belongs to
   rather than above the whole block. */
/* Inline beside the weapon it qualifies, not floated to the block's right margin.
   Ben, 2026-09-12. The float put it nowhere near the thing it explains and stacked a
   column of identical circles down the edge of a ten-model squad. */
.qualifier { display: inline-flex; vertical-align: middle; margin: 0 0 0 6px; }
/* The `i`, and it overrides the underlined-word treatment `.explains` carries on a
   keyword: this one is a symbol in a circle rather than a word in a sentence. */
/* Smaller and in the accent, which is the app's amber in the dark theme Ben reads it
   in and its steel blue in the light one. `--accent` rather than a new colour, because
   this file has exactly one accent in two roles and the contrast of both is recomputed
   on every test run; a hardcoded orange would be a third colour nothing checks.

   **18px drawn, 34px tappable, and the two are different boxes.** Padding was the first
   attempt and it grew the circle instead of the target, because the border is on the
   padding box: the symbol came out at 36px, which is what it was before. The hit area
   is an invisible pseudo-element over the top, so the circle can be small without the
   control being hard to hit with a thumb. */
.qualifier > .explains {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  flex: none;
  padding: 0;
  background: none;
  border: 1px solid var(--accent);
  border-radius: 50%;
  color: var(--accent);
  font-size: var(--t-micro);
  font-weight: 700;
  font-style: italic;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
}
.qualifier > .explains::after { content: ""; position: absolute; inset: -8px; }
.qualifier > .explains:hover { background: var(--inset); }

/* A dropdown block has no item name to sit beside, so the symbol goes on the control's
   own line rather than under it. One row, the select taking the slack. */
.loadout .choice-row { display: flex; align-items: center; gap: 6px; }
.loadout .choice-row > select { flex: 1 1 auto; min-width: 0; }

/* --- chunk H: the build sequence -------------------------------------------------- */

/* The stepper. Numbers rather than names: five words do not fit a phone's width, and
   the screen below each step already carries its own heading. */
.stepper { display: flex; gap: 6px; margin: 10px 0 4px; }
.stepper > .chip {
  flex: 1;
  min-height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* No opacity here. The stylesheet is allowed exactly one opacity rule and a test
   enforces it, because ten of the fifteen states the design brief defined by opacity
   failed WCAG. `--disabled-ink` is a measured colour and carries this on its own. */
.stepper > .chip[disabled] { color: var(--disabled-ink); }

.setup .section { margin-top: 4px; }
.summary .line .name { color: var(--ink); }

/* The DP allowance, pinned above the detachments where it is spent. */
.allowance {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 8px 10px;
  margin: 0 0 10px;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
}
.allowance .figure { font-weight: 700; }
.allowance .of { color: var(--ink-faint); font-weight: 400; }

/* A group heading with its own plus. Item 36. */
.group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.group-head .section { margin-bottom: 0; }
.group-head > .step { min-width: 34px; min-height: 34px; }

/* The allied points allowance, beside its group's plus. Item 37.

   The DP line on the detachment step is a block with the same class; this one sits
   inline in a flex row, so it drops the block padding and the border rather than
   getting a class of its own. `margin-left: auto` pushes it to the right of the
   heading and leaves the plus at the end, which is the official app's roster row. */
.group-head > .allowance {
  margin: 0 0 0 auto;
  padding: 0 8px 0 0;
  background: none;
  border: 0;
  font-size: var(--t-body);
  font-weight: 700;
}
.group-head > .allowance .of { font-weight: 400; color: var(--ink-faint); }

/* The sentence the allowance came from, under the heading it applies to. Printed at
   reading weight because it is the publisher's, not a hint. */
.lines-group > .quoted[data-allied-rule] {
  margin: 0 0 8px;
  padding: 8px 10px;
  background: var(--inset);
  border-left: 3px solid var(--line);
  border-radius: 0 var(--radius-small) var(--radius-small) 0;
  font-size: var(--t-body);
}

/* How many of a unit to add, in the panel. Item 28. */
.counter { display: inline-flex; align-items: center; gap: 4px; }
.counter .n { min-width: 18px; text-align: center; font-weight: 600; }

/*
  The findings banner: a strip above the tab bar, and the app's only alarm.

  Ben, 2026-09-12, replacing the verdict strip, the per-row badge (item 29) and the
  unattached findings list with one thing. `position: fixed` at the tab bar's own
  height, so it sits ON the bar's top edge and the two read as one furniture stack
  rather than as a strip that happens to be near the bottom.

  **It reserves its own height**, which is the chunk G lesson about the bar itself
  arriving a second time: a fixed element that does not is an element that covers the
  last row of the thing it is reporting on. `.builder[data-banner="true"]` pays for it,
  so the padding exists exactly when the strip does and a legal roster gets its space
  back.
*/
.findings-banner {
  position: fixed;
  left: 0; right: 0;
  bottom: calc(var(--tabbar) + env(safe-area-inset-bottom, 0px));
  z-index: 19;
  display: flex;
  background: var(--raised);
  border-top: 1px solid var(--line);
}
.findings-banner > button {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--tap);
  padding: 0 16px;
  background: none;
  border: 0;
  color: var(--bad);
  font-family: inherit;
  font-size: var(--t-body);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  cursor: pointer;
}
.findings-banner[data-verdict="INCOMPLETE"] > button { color: var(--unfinished); }
/* The count, so "two things are wrong" is answerable without opening the box. */
.findings-banner .n {
  min-width: 20px;
  padding: 0 6px;
  border: 1px solid currentColor;
  border-radius: 999px;
  font-size: var(--t-small);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 18px;
  text-align: center;
}
.builder[data-banner="true"] { padding-bottom: var(--tap); }

/* One line where a screenful of struck-through rows used to be. */
.refused-count {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 12px 0 0;
  color: var(--ink-dim);
  font-size: var(--t-body);
}
/* The `i` beside a thing it explains, wherever that thing is. `.qualifier` was the
   loadout's own and is now every clarification's, so the layout rules move with it. */
.filtered { display: flex; align-items: center; gap: 6px; }
.filtered .key { margin: 0; }
.grant-notes p {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: var(--t-body);
  color: var(--ink-dim);
}
.grant-notes p + p { margin-top: 6px; }

.loadout-unread .qualifier { margin-left: 6px; }
.detachments .qualifier { margin-left: 0; }

/* Findings inside the banner's dialog, grouped under the unit each is about. */
.finding-group + .finding-group { margin-top: 12px; }
.finding-group .who {
  margin: 0 0 6px;
  font-size: var(--t-body);
  font-weight: 700;
}
.dialog-body .finding { margin-top: 6px; }
.dialog-body .finding .chips {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 6px 0 0;
}

/* What an attached leader brings to the unit it is leading. Its own headed block
   rather than merged into the unit's list: the two printed cards stay
   distinguishable, and a flat list would match neither. Ben, 2026-09-13. */
.attached-abilities {
  margin-top: 10px;
  padding-left: 10px;
  border-left: 2px solid var(--line);
}
.attached-abilities > .label { margin: 0 0 4px; }

/* The strengths this roster actually chose, above the range the card prints. The
   published line stays because it is the source a reader compares against. */
.composition .actual { margin: 0 0 4px; }
.composition .published { color: var(--ink-faint); }

/* --- chunk E: how a rule is presented ---------------------------------------------

   Ben's Document 2, bottom half. The detachment popup and, by E6, the army rules
   popup with it. */

/*
  **An enhancement is a name, a cost on the right, and its rule one tap in.** E3 and E4,
  and they are one change: a detachment with six enhancements was six paragraphs of rules
  text in a box you opened to decide which detachment to take, which is the wrong
  question at the wrong moment.

  The same row shape as everything else in this app that has a name and a number, which
  is what Ben means by "as the unit banners do it". Three columns and the `i` in the
  middle, so the cost stays against the right edge whatever the name's length: the same
  placement bug the picker had in chunk D, avoided here by having seen it.
*/
.enhancements { columns: 1; }
/*
  THREE cells, because the `i` sits between the name and the cost, and `.line` is a
  two-column grid. That is the whole of this rule's job.

  What puts the cost on the right is `.line .points { text-align: right }`, which is
  app-wide and already there. The first version of this block also carried
  `justify-self: end` and an explicit `grid-column` on each cell, and the mutation pass
  showed that none of the three could fail on its own: flip the template and the number
  still lands against the right edge, because the app-wide rule right-aligns the text
  inside whatever column it gets. Three rules agreeing is three rules, and only one of
  them is the reason. P2, and the same redundancy the welcome screen had in chunk B.
*/
.enhancement { grid-template-columns: minmax(0, 1fr) auto auto; align-items: baseline; }

/*
  **A stratagem card, E5.** Banner in the stratagem colour, white type, CP at the right.

  The whole banner is the control, because at a table with dice in the other hand a row
  is easier to hit than a control inside a row.

  `data-turn` carries the colour, and the words for it are printed on the banner beside
  the category. The publisher draws this rule and never writes it; a screen that copied
  only the drawing would be repeating the puzzle rather than answering it, and would fail
  WCAG 1.4.1 into the bargain.
*/
.stratagems { display: grid; gap: 8px; margin-top: 4px; }
/* A block that names one of its faction's army rules, and the link to it (0.9.3). */
.names-army-rule { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
  margin: 8px 0 0; font-size: var(--t-small); }
.names-army-rule .key { color: var(--ink-dim); }
.stratagem-named { display: grid; gap: 4px; }
.strat-banner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px 10px;
  width: 100%;
  padding: 8px 12px;
  border: 0;
  border-radius: var(--radius);
  background: var(--inset);
  color: var(--on-strat);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.stratagem[data-turn="your"] .strat-banner,
.strat-head[data-turn="your"] { background: var(--strat-your); }
.stratagem[data-turn="either"] .strat-banner,
.strat-head[data-turn="either"] { background: var(--strat-either); }
.stratagem[data-turn="enemy"] .strat-banner,
.strat-head[data-turn="enemy"] { background: var(--strat-enemy); }

.strat-name {
  font-size: var(--t-lead);
  font-weight: 700;
  line-height: 1.25;
  overflow-wrap: break-word;
}
/* CP at the right, and the TEMPLATE is what puts it there: the name column is the
   flexible one and this shrink-wraps. It also carried `justify-self: end`, which could
   not fail while the template held, so it is gone for the reason above. */
.strat-cp {
  font-size: var(--t-lead);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* The category and whose turn, under the name and across both columns: two facts that
   belong together and neither of which is the thing you scan for.

   Quieter by SIZE, not by opacity. It was `opacity: 0.85` for half an hour and the
   structural test caught it: this app allows exactly one opacity rule, the whole-screen
   busy state, because every other faded thing in the previous app's brief was a control
   somebody had to read and could not. On three different banner colours there is no one
   dimmed white that clears 4.5 on all of them, and the hierarchy this needed was never
   the colour anyway. */
.strat-kind {
  grid-column: 1 / -1;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

/* The banner repeated as a strip at the head of the open card, so the colour and the
   cost are still there while the card covers the list it came from. */
.strat-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 0 0 10px;
  padding: 6px 12px;
  border-radius: var(--radius);
  color: var(--on-strat);
}
.strat-head .strat-kind { grid-column: auto; }
/* The `i` last on the strip, pushed right, so it is attached to the strip
   rather than floating above the first field. */
.strat-head .qualifier { margin-left: auto; }
.strat-head .explains { color: var(--on-strat); }

/* WHEN, TARGET, EFFECT and RESTRICTIONS are the publisher's own field labels and Ben
   asked for them in bold. They are already `.field .key` everywhere else in this app;
   this is the one place they are the structure of the rule rather than a note about
   it. */
.strat-body .field { margin: 0 0 8px; }
.strat-body .field .key {
  display: block;
  margin-bottom: 1px;
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
}

/* Flavour behind the `i` reads as prose, not as a rule: the one place in this app where
   italics are right, because the publisher sets it that way and it is the only text in
   the box that is not a rule. */
/* Same rule: the publisher's flavour, behind the `i`, upright. */
.qualifier .flavour {
  margin: 0;
  color: var(--ink-dim);
}

/* --- chunk F: the datasheet and the popups it opens -------------------------------

   Ben's Document 4. */

/*
  **The masthead answers two questions before you read anything, F3 and F4.**

  The name on the left, what the unit is made of on the right, and one line under the
  name saying whether the table below is the whole table. Both were a section each,
  further down the page, and both are things you want before the statline rather than
  after the abilities.

  The composition is allowed to shrink and the name to take the space, because a name is
  never truncated (P11) and a composition is two short lines.
*/
/*
  Wrapping, and the composition drops to its own line rather than squeezing the name.

  `nowrap` was tried first and it is the wrong trade: at 411px it left "Plague Marines"
  on two lines and the composition on four, both of them ragged, to keep two blocks side
  by side. P11 is that a unit name is never truncated, and its spirit is that the name is
  the thing the screen is about. The composition's own lines do NOT wrap, so it is a
  block of a known width that either fits beside the name or goes under it, right-aligned
  either way, which is what "top right" means on a phone.
*/
/*
  **The masthead draws its own rule, and the filtered line sits above it.**

  Ben, 2026-09-16, on the datasheet popup: "move 'Showing equipped loadout only' to
  underneath the title so it sits above the line." The rule used to be the DIALOG's, at
  the top of the box under a heading that repeated the unit's name; with that heading
  gone the box opens straight onto the name, and the divider belongs where the head of
  the sheet ends rather than where the box begins. Everything that answers "what is this
  unit and is this table the whole table" is now above one line, and the characteristics
  start below it.

  Present on the Datasheets surface too, where the sheet has always been the whole
  screen: the same three things are being separated from the same table.
*/
.sheet-head {
  padding-bottom: 9px;
  border-bottom: 1px solid var(--line);
}
.sheet-head .title-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 12px;
}
/*
  **P11 on the datasheet masthead, and the wrap mode was not the cause.**

  At 411px "Onager Dunecrawler" set as "Onager Dunecrawle / r". The obvious read is
  `overflow-wrap: break-word`, and changing it to `break-word` did nothing, because
  `break-word` still breaks a word that cannot fit its column ON ITS OWN. The column was
  `10ch` and "Dunecrawler" is eleven characters, so the word never fit and every wrap
  mode was a last resort.

  The basis is the fix. `18ch` is wide enough for the longest single word on a datasheet
  and the row already wraps, so a title that needs the whole width takes it and the
  UNIT COMPOSITION block drops to the line underneath. `break-word` stays as the honest
  backstop for a word longer than the phone is wide.

  Two lessons, both worth the space: a wrap mode cannot save a column narrower than a
  word, and this was caught by looking at the screen rather than by any test. The test
  that now holds it measures the rendered words, not the declaration, because the
  declaration was the thing that looked right and was not the problem.
*/
.sheet-head h1 { margin: 0; flex: 1 1 18ch; min-width: 0; overflow-wrap: break-word; }
.sheet-head .composition {
  flex: 0 0 auto;
  margin-left: auto;
  text-align: right;
  font-size: var(--t-small);
  white-space: nowrap;
}
.sheet-head .composition .label {
  display: block;
  margin: 0 0 1px;
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}
.sheet-head .composition ul { list-style: none; margin: 0; padding: 0; }
.sheet-head .composition .published { color: var(--ink-faint); }

/* The filtered line, in the masthead and no longer a section. One sentence and the `i`
   that holds the names; the LABEL stays in the open because C5a is that an instrument
   says it is filtered, and what goes behind the tap is the detail. */
.sheet-head .filtered {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 4px 0 0;
  font-size: var(--t-small);
  color: var(--ink-dim);
}
.sheet-head .filtered .key { color: var(--ink-dim); }

/*
  **Who can lead this unit, closed by default, F7.** Nine names on Plague Marines, sitting
  open between the abilities and the keywords on every read. The summary is a tap target
  because it is the control.
*/
.led-by > summary {
  min-height: var(--tap);
  display: flex;
  align-items: center;
  color: var(--ink-dim);
  font-size: var(--t-body);
  font-weight: 600;
  cursor: pointer;
}
.led-by[open] > summary { color: var(--ink); }

/* The `i` beside the Keywords heading, F8, rather than a line of text under the chips
   saying what the control would do. */
/* The `i` sits ON the heading line. `.refs .explain` makes an Explain a block, which is
   right where it is a paragraph of its own and wrong here. */
.sheet-read .label > .explain,
.refs .label > .explain { display: inline-flex; margin: 0 0 0 6px; vertical-align: middle; }
.explains.icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border: 1px solid var(--accent);
  border-radius: 999px;
  color: var(--accent);
  font-size: var(--t-micro);
  font-style: italic;
  font-weight: 700;
  text-decoration: none;
}

/* The Designer's Note, set apart from the rule it clarifies, F9. Italic and quieter: it
   is the publisher explaining the rule rather than more of it. It is still SHOWN, which
   was Ben's earlier decision and the right one. */
/* The publisher clarifying its own rule. Set apart by a rule down its left edge since
   2026-09-16 rather than by italic, which now means the app is speaking. F9 put it
   apart deliberately and that has not changed; only the mechanism has. */
.designer-note {
  margin-top: 10px;
  padding-left: 10px;
  border-left: 2px solid var(--line);
  color: var(--ink-dim);
}

/* The grant, one line under the keyword's title, F10. */
.granted-by { margin: 0 0 10px; font-size: var(--t-body); color: var(--ink-dim); }

/* Wargear abilities under the weapon tables, F5. Tight to the table above, because they
   belong to it: they are things a model in this unit is carrying. */
.wargear-abilities { margin-top: -4px; }

/* --- chunk H: enhancements -----------------------------------------------------------

   Ben's Document 3, grouped separately because two of the three were rules questions. */

/*
  **A list, not a dropdown.** It was a `select` until 2026-09-13 and chunk H asks for
  three things a `select` cannot do: strike a refused row through (H1), label it, and
  expand the chosen one to show what it does (H4).
*/
.enhancements-pick { display: block; }
.enhancement-list { columns: 1; margin-top: 4px; }
.enhancement-row {
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: baseline;
  gap: 0 8px;
}
.enhancement-row > .name {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--t-lead);
  font-weight: 600;
  text-align: left;
  color: var(--ink);
  cursor: pointer;
  overflow-wrap: break-word;
}
.enhancement-row > .name:hover { color: var(--accent); }

/* The one this unit holds. A pressed state has to be more than a colour, so it takes
   the accent AND a rule down its left edge: WCAG 1.4.1, and the same reason the active
   tab carries a line as well as its colour. */
.enhancement-row[data-taken="true"] {
  border-left: 2px solid var(--accent);
  padding-left: 8px;
}
.enhancement-row[data-taken="true"] > .name { color: var(--accent); }

/*
  **Struck through and labelled, H1**, and this is Ben's deliberate reversal of his own
  hide-what-cannot-be-selected rule, for this case only. It earns the exception because
  the refusal is about the ROSTER rather than about the unit: "you cannot take this
  because you already have it" is something you want to know while deciding, and a list
  that silently shortens as you build explains nothing.

  Struck through AND in the faint ink AND carrying the words, because a strike-through
  alone is a visual-only signal and the label is what a reader who cannot see it gets.
*/
.enhancement-row.in-use > .name {
  color: var(--ink-faint);
  text-decoration: line-through;
  cursor: not-allowed;
}
.enhancement-row.in-use > .points { color: var(--ink-faint); font-weight: 400; }
.in-use-label {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
  white-space: nowrap;
}

/* **What the one you took does, H4.** On the taken row only: this box is where you
   choose between four of them, and four paragraphs is the wall chunk E took out of the
   detachment popup for the same reason. */
.enhancement-row > .effect {
  grid-column: 1 / -1;
  margin: 4px 0 2px;
  font-size: var(--t-body);
  color: var(--ink-dim);
}

/*
  **What a Force Disposition does**, added 2026-09-14 with the mission deck capture.

  Five rows under the disposition control: the primary mission each opponent's
  disposition produces. Same `.line` grid as every other name-and-detail row in the app,
  so it reads as a list of answers rather than a new kind of block, and the mission NAME
  carries the weight because that is the thing being looked up.

  The provenance line sits in `.wnotes` like the note above it, and is deliberately not
  a clause chip: a chip means the printed Core Rules, and this is a mission pack.
*/
.disposition-missions { margin: 4px 0 0; }
.disposition-missions .lines { columns: 1; }
.mission-row {
  grid-template-columns: minmax(0, auto) 1fr;
  align-items: baseline;
  gap: 0 8px;
  padding: 5px 0;
}
/* A mission's name is a row name like every other row name, so it takes --t-lead with
   them. It carried 14px and 0.01em of its own, which is the kind of near-miss the
   scale exists to remove: one hundredth of an em is not a decision anybody made. */
.mission-row > .name {
  font-size: var(--t-lead);
  font-weight: 600;
}
.mission-row > .detail {
  color: var(--ink-faint);
  font-size: var(--t-small);
  text-align: right;
  /* The opponent is the KEY you look this up by, so it never truncates to an ellipsis
     the way a value might: P11's rule, one screen further on. */
  overflow-wrap: break-word;
}
.disposition-missions > .key { margin: 0 0 2px; }

/*
  **What a granted save is**, added 2026-09-14 with `datasheets/saves.py`.

  Rows inside the existing unchecked-finding block, above the publisher's sentences
  rather than instead of them: the line is the summary and the quote underneath is the
  rule, and a reader has to be able to get from one to the other without leaving.

  A save that needs a roster or a battlefield is quieter than one the datasheet answers
  on its own, because the difference between "this unit has a 4+" and "this unit gets a
  4+ when a character is leading it" is the whole reason this block exists.
*/
.granted-saves { columns: 1; margin: 4px 0 6px; }
/* Stacked, not two columns. "4+ invulnerable save" and "units within range of it,
   against ranged attacks" are one statement in two parts, and at 411px the second part
   wraps; right-aligning a wrapped phrase under a left-aligned one reads as two unrelated
   things. Seen on screen on the Onager Dunecrawler before it was written this way. */
.save-row {
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
  padding: 3px 0;
}
.save-row > .name { font-size: var(--t-body); font-weight: 700; }
.save-row > .detail {
  color: var(--ink-dim);
  font-size: var(--t-small);
  text-align: left;
}
.save-row[data-needs-roster="true"] > .name { color: var(--ink-dim); font-weight: 600; }

/*
  **A table the publisher prints as a picture**, 2026-09-14. Death Guard's Contagion
  Range and nothing else in the store.

  Same `.line` grid as every other name-and-number row in the app, because it IS one: a
  battle round on the left and a distance on the right. What it does not look like is
  the rest of the rule text around it, which is the point: this is a table, and it was
  drawn as a table on the page it came from.

  The provenance line under it is `.wnotes` like every other note this app serves, and it
  is never omitted: these three numbers were read off pixels, and a table that looked
  exactly like a parsed one would be claiming more than the project has.
*/
.image-table { margin: 6px 0; }
.image-table > .key { margin: 0 0 2px; }
.image-table .lines { columns: 1; }
.band-row {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0 8px;
  padding: 3px 0;
}
.band-row > .name { font-size: var(--t-body); font-weight: 600; }
.band-row > .points { font-size: var(--t-body); font-weight: 700; }

/*
  **The publisher's own emphasis inside a rules body, 2026-09-18.**

  Ben's mark-up item 47 asked whether phases, ability names and faction names should be
  bolded inside a sentence. Wahapedia already bolds them, and the capture was stripping
  it; `A2` forbids this project deciding which words in somebody else's sentence are
  special, so what reaches here is the publisher's decision rather than one of ours.

  **600 rather than 700**, from the ladder rather than beside it. 2,195 of these appear
  across 1,298 ability blocks, several to a paragraph on a busy one, and the top rung is
  what a row NAME takes: at 700 an ability would read as a run of headings with sentence
  fragments between them. 600 separates the term from the sentence and lets the sentence
  stay a sentence.

  **Upright, not italic.** `em` is the right element, because this is the publisher
  stressing a word, and italic is already spoken for: the loadout screen uses it for this
  app's own voice against the publisher's roman, which is the distinction Ben chose on
  2026-09-16. Setting a quoted term in italic here would put the publisher's words into
  the app's voice on a different screen.
*/
.marked.emph {
  font-style: normal;
  font-weight: 600;
  color: inherit;
}

/*
  A marked phrase the publisher LINKED carries the accent and nothing else, which is the
  same treatment a tappable keyword takes: `.explains` supplies it, because the control
  inside is the same component. The wrapper exists to carry the data attributes, so it
  must not add a second visual event on top of the one inside it.
*/
.marked { display: inline; }

/*
  The stat line and the one caveat about its printed INV number.

  Ben's item 24: the block that used to hold this is gone, and the sentence it carried is
  the only part that said something no other part of the screen does. It sits under the
  numbers because it is about one of them.
*/
.statline-wrap > .save-caveat {
  /* Bottom margin as well as top, and it is not symmetry for its own sake: the first
     screenshot had the caveat sitting hard against the RANGED WEAPONS heading below it,
     so it read as a note about the weapons table rather than about the numbers above. */
  margin: 6px 0 14px;
  color: var(--ink-dim);
  font-size: var(--t-small);
}

/*
  **The Faction Rules screen, item 7, 2026-09-19.**

  Almost nothing new, which is the point: the screen reuses `.abilities`, `.ability`,
  `.label`, `.who` and `Prose` from the datasheet, so a rule reads the same way wherever
  you meet it. What it needs of its own is the detachment row, which is a disclosure and
  a cost on one line.
*/
.detachment-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}
.detachment-row:last-child { border-bottom: 0; }
.detachment-row > .name {
  flex: 1 1 auto;
  min-width: 0;
  /* `--t-body` until 2026-09-22, so the same role measured 13px here and 15px on
     `.line .name`. Option C: one treatment per role, and `--t-lead` is what the scale
     documents for a row name. */
  font-size: var(--t-lead);
  font-weight: 600;
  text-align: left;
}
/* **The accent means a thing you can touch or the state you are in**, which
   `styles.css` records at `.det.on`. Gameplay painted its resting row names gold, so on
   that one screen the accent meant "this is a row" while everywhere else it meant "this
   is the one you chose". `.explains` supplies the accent to every tappable thing and is
   right for an `i` or a clause chip; a row name is not one of those. Ben chose this
   explicitly on 2026-09-22, having been shown it separately because it is the visible
   half of option C. */
.detachment-row > .name.explains { color: var(--ink); }
/* The cost on an enhancement or a stratagem, which sits after its name on the same
   line. A margin rather than a space in the markup: "Final Ingredient+20 pts" was what
   the first pass drew, and a space inside a template is the kind of thing that vanishes
   in a reformat. */
.ability > .who > .detail {
  margin-left: 8px;
  font-weight: 400;
  color: var(--ink-dim);
}
.detachment-row > .detail {
  margin-left: auto;
  font-size: var(--t-small);
  color: var(--ink-dim);
  white-space: nowrap;
}

/* The faction index rows are the same list the armies index uses. */
.faction-rules .masthead { margin-bottom: 4px; }


/*
  **Item 21's two reinstated blocks**, browse sheet only. Both take the `.refs` treatment
  the attachments block already has, because they are the same kind of thing: a published
  block quoted rather than acted on.
*/
.composition-block .published { list-style: none; margin: 0; padding: 0; }
.composition-block .published li { font-size: var(--t-body); padding: 1px 0; }
/* `.message` is scoped to `.abilities`, `.wargear` and `.quoted`, so a `.message` in a
   new block falls through to the browser's 16px paragraph. Caught by measuring the
   computed style rather than by looking at it, which is how the 16px would have shipped:
   at a glance it reads as emphasis rather than as a missing rule. */
.composition-block .message,
.wargear-options .message {
  margin: 1px 0 0;
  font-size: var(--t-body);
  color: var(--ink-dim);
}
.composition-block .kit { margin-top: 8px; }
/*
  **Not the uppercase label treatment**, which is what this line first took and which was
  wrong for a reason worth writing down: "Every model is equipped with:" is the
  PUBLISHER'S sentence, captured as `who` plus the words this app rejoins it with, and
  `A2` says quoted text is not restyled. Shouting half a printed sentence is a restyling.
  It is a sentence, in the sentence weight, and the uppercase micro treatment stays for
  the headings this app writes itself.
*/
.composition-block .kit .who {
  color: var(--ink-dim);
  font-size: var(--t-body);
  font-weight: 600;
  margin: 0 0 2px;
}
.composition-block [data-composition-alternative] { margin-top: 6px; }
.wargear-options .bullets { margin: 0; padding-left: 18px; font-size: var(--t-body); }
.wargear-options .bullets > li { margin-bottom: 6px; }
.wargear-options .choices { margin: 3px 0 0; padding-left: 16px; color: var(--ink-dim); }

/* A published attachment permission the card prints as a sentence rather than a list.
   26 of them, all `ATTACHED UNIT`; see `units.js`. `.message` is scoped, so it is named
   here for the same reason the composition block's is. */
.attachments .attachment-sentence .message {
  margin: 0 0 8px;
  font-size: var(--t-body);
  color: var(--ink-dim);
}

/*
  **The list summary, rebuilt to the mock-up Ben approved on 2026-09-20.** Items 63 to 67.

  Item 63 is "same fonts through here to match the unit builder itself", so every size and
  weight below is the builder's own row: the name at `--t-body` 700, the count and the
  equipment at `--t-small`, the role heading at the micro uppercase treatment every other
  heading in this app takes. Nothing here introduces a step.
*/
.list-summary .group-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 11px 0 2px;
  border-bottom: 1px solid var(--line);
  padding-bottom: 2px;
}
/*
  **`.label` has no global rule in this app**, it is styled per surface, so an `h3.label`
  in a new block falls through to the browser's bold 1.17em. That is what was drawing the
  role headings and "Share this list" at 18.72px in title case, beside a 13px list.

  Item 63 is "same fonts through here to match the unit builder itself", so both take the
  one treatment every uppercase label in this app takes:
  `--t-micro` / 700 / `--track-caps`, which `test_every_uppercase_label_takes_the_same_
  treatment` enforces across the whole stylesheet.
*/
.list-summary h3.label,
.list-summary .group-head .label {
  margin: 0;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-dim);
}
.list-summary h3.label { margin: 14px 0 6px; }
.list-summary .group-head .label { color: var(--accent); }

/* The warlord marker, the same chip the builder's row carries. */
.summary-head .warlord {
  display: inline-block;
  margin-left: 6px;
  padding: 0 5px;
  border-radius: 4px;
  border: 1px solid var(--accent);
  color: var(--accent);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  vertical-align: middle;
}
.list-summary .group-head .n { margin-left: auto; font-size: var(--t-micro); color: var(--ink-faint); }

/* Compact, which is Ben's "aiming to keep this more compact": two lines for a plain unit
   and three where there is equipment, against four or five before. */
.summary-row { padding: 5px 0 4px; border-bottom: 1px solid var(--line); }
.summary-row:last-child { border-bottom: 0; }
.summary-head { display: flex; align-items: baseline; gap: 8px; }
.summary-head .name { font-size: var(--t-body); font-weight: 700; color: var(--ink); min-width: 0; }
.summary-head .name .n {
  margin-left: 6px;
  font-size: var(--t-small);
  font-weight: 400;
  color: var(--ink-dim);
  white-space: nowrap;
}
.summary-head .points {
  margin-left: auto;
  font-size: var(--t-body);
  font-weight: 700;
  white-space: nowrap;
}

/* The attached character, indented under the unit it joined, with its own count and its
   own points because the validator prices the two separately and P1 says the line items
   sum to the total. */
/* A MARGIN rather than padding, so the indent is the element's own left edge and can be
   measured as one. Padding moved the text and left the box where it was, which reads the
   same and asserts as zero. */
.summary-head.attached { margin-left: 14px; margin-top: 1px; position: relative; }
.summary-head.attached::before {
  content: "";
  position: absolute;
  left: -10px;
  top: 0.15em;
  bottom: 0.15em;
  width: 2px;
  background: var(--edge);
  border-radius: 1px;
}
.summary-head.attached .summary-head.bare { display: flex; width: 100%; }
.summary-head.attached .name { font-weight: 600; color: var(--ink-dim); }

/* Equipment left, enhancement right. A grid rather than a flex row, because the kit list
   has to reach the full width when it is open: closed the two sit side by side as Ben
   asked, open the disclosure takes a row of its own. */
.summary-foot {
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: 10px;
  align-items: baseline;
  margin-top: 1px;
}
.summary-foot details.kit { grid-column: 1; grid-row: 1; min-width: 0; }
.summary-foot details.kit[open] { grid-column: 1 / -1; grid-row: 2; }
.summary-foot .enh {
  grid-column: 2;
  grid-row: 1;
  text-align: right;
  font-size: var(--t-small);
  color: var(--accent);
}
.summary-foot .enh .one { display: block; }
.list-summary details.kit > summary {
  list-style: none;
  cursor: pointer;
  font-size: var(--t-small);
  color: var(--accent);
  min-height: 24px;
  display: flex;
  align-items: center;
}
.list-summary details.kit > summary::-webkit-details-marker { display: none; }
.list-summary details.kit > summary::before { content: "+ "; font-weight: 700; }
.list-summary details.kit[open] > summary::before { content: "\2212 "; }
.list-summary .kitlist {
  margin: 3px 0;
  padding: 6px 9px;
  background: var(--inset);
  border-radius: 7px;
}
.list-summary .kitrow { margin: 0; font-size: var(--t-small); padding: 1px 0; }
/* Where an attachment puts two units' equipment in one disclosure, each gets its name
   over its own rows: a merged list with no owner is a loadout no single model has. */
.list-summary .kitwho {
  margin: 4px 0 1px;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
}
.list-summary .kitwho:first-child { margin-top: 0; }
.list-summary .field.total .points { font-weight: 700; }
.list-summary .field.total .of { color: var(--ink-dim); font-size: var(--t-small); }

/* Item 67: full width, red outline rather than a red fill. A filled red button that wide
   reads as the primary action on the screen, and the primary action here is Share. */
.list-summary .danger {
  display: block;
  width: 100%;
  margin-top: 14px;
  padding: 11px 0;
  border-radius: 10px;
  border: 1px solid var(--bad);
  background: transparent;
  color: var(--bad);
  font: inherit;
  font-size: var(--t-body);
  font-weight: 700;
  text-align: center;
  cursor: pointer;
}

/*
  **Item 41's mission table.** Ben took the published 5x5 matrix and cut it to the column
  you hold: "instead of showing every column we have a selection drop down which changes
  the table to show the selection in the column and the potential match-ups in the rows."

  Borders on every cell, because that is what makes it read as the publisher's table
  rather than as a list with a rule between rows. The mission is the only thing on a row
  worth pressing, so the cell IS the control.
*/
.disposition-missions .pick { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; }
.disposition-missions .pick .key {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-dim);
  white-space: nowrap;
}
.disposition-missions .pick select { flex: 1; min-width: 0; }
/* A chooser with one option is still the chooser, so it keeps the control's height and
   its type and loses only the affordance. **Dashed rather than faded**, and the ink is
   put back explicitly: the browser's own disabled styling dims the text, which is exactly
   what `test_no_state_in_the_stylesheet_is_defined_by_opacity_alone` exists to stop, and
   the state here is "nothing to choose" rather than "you may not read this".
   `-webkit-text-fill-color` as well as `color`, because WebKit fades a disabled control
   through that property and ignores `color` on it. */
.disposition-missions .pick select:disabled {
  color: var(--ink);
  -webkit-text-fill-color: var(--ink);
  border-style: dashed;
}
.mission-grid { width: 100%; border-collapse: collapse; font-size: var(--t-small); }
.mission-grid th, .mission-grid td { border: 1px solid var(--line); padding: 6px 8px; }
.mission-grid thead th { background: var(--inset); color: var(--ink-dim); font-weight: 600; }
.mission-grid thead th.corner { text-align: left; width: 44%; }
.mission-grid thead th.mine { text-align: left; color: var(--ink); font-weight: 700; }  /* ink, not orange: K1, 0.9.3 (the match-ups scene found it) */
.mission-grid tbody th { background: var(--inset); color: var(--ink-dim); text-align: left; font-weight: 600; }
.mission-grid td { padding: 0; }
.mission-grid td .explains {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--tap);
  padding: 4px 8px;
  font-size: var(--t-small);
  font-weight: 700;
  text-align: left;
}
.mission-grid tr.on td { background: rgba(224, 182, 79, 0.10); }

/* The card a mission opens. */
.mission-card .seg { margin: 0 0 10px; }
.mission-card .seg:last-of-type { margin-bottom: 0; }
.mission-card .label { color: var(--accent); margin: 0 0 2px; }
.mission-card .score {
  display: flex;
  gap: 8px;
  align-items: baseline;
  margin: 0 0 3px;
  font-size: var(--t-body);
}
.mission-card .score .what { flex: 1; min-width: 0; }
.mission-card .score .pre {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  color: var(--ink-faint);
  white-space: nowrap;
}
.mission-card .score .vp { font-weight: 700; color: var(--accent); white-space: nowrap; }
.mission-card .score .cumul {
  display: inline-block;
  margin-left: 6px;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* --- Gameplay, item 8 ------------------------------------------------------------- */

/*
  The clause heading over the seven parts of a turn. It is a link because the clause is
  readable in full on the Rules screen, and it is set as a label rather than as a
  paragraph link because it is doing a heading's job on this screen.
*/
/* One treatment for every uppercase label in the app, which
   `test_every_uppercase_label_takes_the_same_treatment` enforces and caught this rule
   inventing its own within the hour. */
.gameplay .label.sub {
  display: block;
  margin: 12px 0 4px;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: none;
}

/*
  A phase row that reaches its section. The whole row is the target, the same decision
  the Add-a-unit row got on 2026-09-18: the name and the line under it are one thing and
  splitting the tap between them gives a reader two ways to miss.

  No underline, because underlining the description as well as the name drew a paragraph
  of underlined body text and read as damage rather than as a link. The name carries the
  accent instead, which is how every other reachable name in this app says so.
*/
.gameplay .turn-part {
  display: block;
  text-decoration: none;
  color: inherit;
  padding: 6px 0;
  border-bottom: 1px solid var(--line);
}
.gameplay .turn-part:last-child { border-bottom: 0; }
.gameplay .turn-part .who { color: var(--accent); }

/* The publisher's step number, ahead of the step's name. */
.gameplay .step-number {
  display: inline-block;
  min-width: 1.4em;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}

/*
  A score line's VP, which is one value on 202 of the pack's 206 lines and TWO on the
  other four: those price Fixed and Tactical apart, and the label rides with the value it
  labels rather than with the line.
*/
.mission-card .vps {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  flex: 0 0 auto;
}
.mission-card .vps .vp-col {
  display: flex;
  align-items: baseline;
  gap: 6px;
  white-space: nowrap;
}
/* Two prices need the labels to say which is which, so the pair is drawn wider. */
.mission-card .vps[data-values="2"] .cumul { display: inline; }

/* The deck's own rows borrow the detachment row, so only the link needs saying. */
.deck .detachment-row > a.name { text-decoration: none; }

/* --------------------------------------------------------------------------
   Tiles. Ben's item 4, 2026-09-22: "keen to show these as a selection of tiles
   (cards) rather than a list."

   **`auto-fill` on a floor, not a fixed column count.** He asked for five across.
   The content column on his phone is 383px (411 less `#app`'s 14px a side), which
   with the 6px gap leaves 71px a tile, and RECONNAISSANCE is 14 characters with
   nowhere to break: at any step in this stylesheet's five-step scale that word
   alone is wider than the tile, and narrowing the type or clipping the name are
   both rules this file is tested against (`P11`). 104px is the floor that fits it
   at `--t-body`, so a phone gets three across, a large phone three, and a tablet
   the five he asked for.
   -------------------------------------------------------------------------- */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: 6px;
  margin: 6px 0 0;
}

.tile {
  position: relative;
  display: flex;
  align-items: stretch;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  background: var(--raised);
}

.tile-name {
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--tap);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 7px 6px;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: var(--t-body);
  /* Centred, which is the half of item 4 that is not the grid: "fonts centered". */
  text-align: center;
  cursor: pointer;
}

/* The name wraps rather than shortens. A mission is identified by its whole name and
   half of one is a different card. `P11`. */
.tile-text { overflow-wrap: break-word; }

/* The word that rides with the colour.

   **Item 5 asks for green and red, and this is the reason they are not alone.** The
   register's own note on that item says the fade must not be the only signal because a
   faded tile is still selectable, and the same argument applies to the marked ones: a
   tile that is only green says nothing to a reader who cannot separate the two hues.
   So the tile prints whose it is, in the same two words the drop-downs are labelled
   with. */
.tile-mark {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Yours and theirs. `--good` and `--bad` are the verdict colours, and this IS a
   verdict about the result: with both dispositions named, exactly one of the 25 is the
   mission you play. `P7` reserves emphasis for an outcome that changes the result. */
.tile.is-yours { border-color: var(--good); }
.tile.is-yours .tile-mark { color: var(--good); }
.tile.is-theirs { border-color: var(--bad); }
.tile.is-theirs .tile-mark { color: var(--bad); }

/* Faded by ink and border, never by `opacity`: a state defined by opacity alone is the
   thing `test_no_state_in_the_stylesheet_is_defined_by_opacity_alone` exists to refuse,
   and ten of the fifteen the brief defined that way failed contrast. It stays a button
   and it still opens its card, because Ben: "all will still be selectable". */
.tile.is-faded { border-color: var(--line); background: var(--ground); }
.tile.is-faded .tile-name { color: var(--ink-dim); }

/* Chosen, on the Play screen's secondaries. The accent, which in this app means the
   thing you touched or the state you are in, the same as `.det.on`. */
.tile.on { border-color: var(--accent); }

/* The `i` sits in the corner rather than in the flow, so a two-line name and a
   one-line name put it in the same place on both. */
.tile > .qualifier {
  position: absolute;
  top: 2px;
  right: 4px;
}

.deck-filter .pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.deck-filter .field { display: block; margin: 0; }
.deck-filter .field > span {
  display: block;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 3px;
}

.deck-filter select {
  width: 100%;
  min-height: var(--tap);
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  background: var(--ground);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-body);
}

/* --------------------------------------------------------------------------
   The attack roller. Ben's 2D stepped layout, 2026-09-22.

   Ben, on the previous app's screen he asked to have back: "No dice tray as a
   separate element - the dice in the screenshot I added will show exactly as in
   that image." So the dice are a row inside the step, not a surface of their own,
   and nothing here animates.
   -------------------------------------------------------------------------- */

.roll .setup {
  display: grid;
  gap: var(--gap);
  margin: 0 0 var(--gap);
}

.roll .side {
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px;
}

.roll .side > .label,
.roll .sequence,
.roll .chose .label,
.roll .unmodelled .label {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 8px;
}

.roll .pick {
  display: block;
  width: 100%;
  min-height: var(--tap);
  text-align: left;
  background: var(--inset);
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  color: var(--ink);
  font-size: var(--t-body);
  padding: 0 10px;
  margin: 0 0 10px;
}

/* **Item R1's banner, 2026-09-23.** Ben: *"The counts can go in the banner ... select
   plague marines, then plasma gun, then add a qty in."*

   The weapon and how many of it are firing, on one line. The count was the first of six
   cells in the profile row below, where it sat beside A, S, AP and D and read as a
   characteristic of the weapon rather than as a fact about the unit. The pick takes the
   rest of the width because a weapon name is long and a count is two digits. */
.roll .banner {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  margin: 0 0 10px;
}
.roll .banner .pick { margin: 0; flex: 1 1 auto; min-width: 0; }
.roll .banner .field.qty { flex: 0 0 68px; }

.roll .fields {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.roll .field {
  flex: 1 1 56px;
  min-width: 56px;
}

.roll .field .label {
  display: block;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 3px;
}

.roll .field input {
  width: 100%;
  min-height: var(--tap);
  background: var(--inset);
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  color: var(--ink);
  font-size: var(--t-body);
  text-align: center;
  padding: 0 4px;
}

.roll .abilities,
.roll .switches {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 10px 0 0;
}

/* A step: the number, the title, and the derived value pinned right. */
.roll .step {
  border-top: 1px solid var(--line);
  padding: 14px 0 12px;
}

.roll .step header {
  display: flex;
  align-items: center;
  gap: 10px;
}

.roll .step .num {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  border: 1px solid var(--line);
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--t-micro);
  color: var(--ink-dim);
}

.roll .step h3 {
  flex: 1 1 auto;
  margin: 0;
  font-size: var(--t-lead);
  font-weight: 700;
  color: var(--ink);
}

/* The answer this step produced, read off the header rather than hunted for. */
.roll .step .derived {
  flex: 0 0 auto;
  font-size: var(--t-small);
  font-weight: 700;
}

.roll .step .derived .on { color: var(--accent); }
.roll .step .derived .off {
  color: var(--ink-faint);
  font-weight: 400;
  margin-left: 6px;
}

.roll .step .why {
  margin: 4px 0 0 30px;
  font-size: var(--t-small);
  color: var(--ink-dim);
}

.roll .step .headline {
  margin: 8px 0 0 30px;
  font-size: var(--t-body);
  color: var(--ink-dim);
}

.roll .step .headline b {
  font-size: var(--t-display);
  font-weight: 700;
  color: var(--accent);
  margin-right: 6px;
}

.roll .step .dice {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 10px 0 0 30px;
}

/* One die: a 3x3 pip grid, flat. */
/* **A die is a die on every screen that draws one, since 2026-09-22.**

   These rules were `.roll .die` until Ben found the roll-offs on the setup screen
   drawing nothing: `play.js` had copied the markup and could not copy the styling, so
   two dice sat in the DOM as unstyled inline spans with nine unstyled children. The
   component moved to `ui.Die` and the geometry came out from under `.roll` with it.
   `test_a_die_is_the_same_die_on_every_screen_that_draws_one` measures the box. */
.die {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  gap: 2px;
  width: 30px;
  height: 30px;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--inset);
  box-sizing: border-box;
}

.die .pip {
  border-radius: 50%;
  background: transparent;
}

.die .pip.on { background: var(--ink); }
/* A missed die is dimmed by its INK and its fill, never by opacity: the
   stylesheet guard is right that a faded control is a control somebody
   could not read, and `opacity: 1` here was a no-op written out of habit. */
.die.is-dim { border-color: var(--line); background: transparent; }
.die.is-dim .pip.on { background: var(--ink-faint); }

/* Gold only where a rule on this weapon keys off the critical, P7. */
.die.is-crit {
  background: var(--fill);
  border-color: var(--fill);
}
.die.is-crit .pip.on { background: var(--onfill); }

/* --------------------------------------------------------------------------
   Item 12: the motion, and the contrast. Ben's rulings on the round-two mock-up,
   2026-09-22: *"C is my favourite on the animations"* and *"On the contrast, I like 3."*

   **Motion C: a quarter turn, both ways.** In from minus ninety, past zero the other
   side, and settled. It reads as a die being shaken and put down rather than placed.
   230ms each, 26ms apart, so the tenth die of a ten-dice tray lands at 464ms; round
   one's tumble was 840ms and he asked for quicker.

   **`P7` is what stops this being decoration.** Every die moves the same way, and only a
   die that changed the result gets a second beat. That is the same rule the gold already
   follows, and it is why a tray of ten misses is quiet.
   -------------------------------------------------------------------------- */

.roll .dice .rolling {
  display: inline-flex;
  animation: die-shake 230ms cubic-bezier(.3, .9, .35, 1) backwards;
  animation-delay: calc(var(--at, 0) * 26ms);
}

@keyframes die-shake {
  from { opacity: 0; transform: rotate(-90deg) scale(.88); }
  55%  { opacity: 1; transform: rotate(14deg); }
  80%  { transform: rotate(-5deg); }
  to   { opacity: 1; transform: none; }
}

/* The beat, on a critical only. Starts after the die is legible, so it costs nothing to
   read: the number is already there and this says which one mattered. */
.roll .dice .rolling:has(.die.is-crit) {
  animation: die-shake 230ms cubic-bezier(.3, .9, .35, 1) backwards,
             die-earned 420ms 170ms ease-out backwards;
}

@keyframes die-earned {
  0%   { box-shadow: 0 0 0 0 rgba(224, 182, 79, .9); }
  70%  { box-shadow: 0 0 0 8px rgba(224, 182, 79, 0); }
  100% { box-shadow: 0 0 0 0 rgba(224, 182, 79, 0); }
}

/* Somebody who has asked their phone to stop moving things has asked this app too. */
@media (prefers-reduced-motion: reduce) {
  .roll .dice .rolling,
  .roll .dice .rolling:has(.die.is-crit) { animation: none; }
}

/* **Contrast 3: the success is stated.**

   A hit and a miss were the same grey at two lightnesses, `--ink` against `--ink-faint`.
   Hue carries it now as well, and so does the border.

   **Keyed on `[data-status]`, not on the tone.** `ui.Die` gives a die with no status the
   same `is-on` as a hit, so a rule keyed on the tone would paint the setup screen's two
   roll-off dice green. A roll-off die has no success in it: it is a number two players
   compare, and telling one of them their 4 was a pass would be this app inventing a
   rule. Only a die that resolved something carries a status.

   **The recession half of that mock-up option is deliberately not built.** Fading the
   failed die's pips below `--ink-faint` puts them under 3:1 on `--raised` in both
   themes, which is the argument `test_no_state_in_the_stylesheet_is_defined_by_opacity_
   alone` already makes about faded states. `--good` measures 5.38 on `--raised` and 6.23
   on `--ground` in the dark theme, 5.27 and 4.52 in the light one. */
.die.is-on[data-status] { border-color: var(--good); }
.die.is-on[data-status] .pip.on { background: var(--good); }

.die .again {
  position: absolute;
  top: -3px;
  right: -3px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--unfinished);
}

/* Item 12: a step the cursor has not reached is not drawn. `display` rather than
   removing it from the DOM, because the five are one list and a reader who scrolls
   should find them in the engine's order the moment each arrives. */
.roll .step.is-later { display: none; }
/* One tray on screen at a time. The parts line above it stays, because that is the
   answer; the dice are the evidence for an answer already given. */
.roll .step.is-done .dice { display: none; }
.roll .actions.step-on { margin-top: 10px; }
.roll .actions.restart { margin-top: 14px; }

/* Item 17's drawn letter. **The house animation**, `die-shake`, which Ben chose for the
   roller's dice on 2026-09-23: a second motion for the same job (a value the app just
   produced arriving on screen) would be a second answer to a settled question. The
   `key` on the element is what makes it replay when the letter changes. */
.play .drawn-letter {
  display: inline-block;
  font-size: var(--t-display);
  font-weight: 700;
  color: var(--accent);
  animation: die-shake 230ms cubic-bezier(.3, .9, .35, 1) backwards;
}
@media (prefers-reduced-motion: reduce) {
  .play .drawn-letter { animation: none; }
}

/* **Item 20's step 7.** One block per unit: the line, the three placements as chips,
   and whatever the datasheet and the engine had to say underneath. No new colours and
   no new controls: a chip is the app's toggle and `.wnotes` is the app's way of printing
   something the publisher wrote or something this app will not rule on. */
.play .formations { display: flex; flex-direction: column; gap: 12px; }
.play .formation {
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.play .formation .line { margin: 0; }
.play .formation .chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; }
.play .formation .chip { min-height: var(--tap); }
/* A control that cannot be pressed says so in the ink, not by disappearing: the reason
   it is unpressable is printed under it and a missing chip would leave that reason
   pointing at nothing. */
.play .formation .chip[disabled] { color: var(--disabled-ink); cursor: default; }
.play .formation .chip[disabled]:hover { border-color: var(--edge); }
.play .formations > .line[data-role="reserved"] { margin: 0; }

/* **Item 16's deployment map.** Ben, 2026-09-23: *"there is no formal PDF available so
   this is the best we'll get."*

   **The plate is light in BOTH themes and that is a legibility claim, not a taste one.**
   Every source has a transparent background, black dimension lines and a black border,
   so on this app's dark surfaces the border and half the dimensions disappear. `--paper`
   is not a token this app has, and inventing one for a single figure would be a sixth
   surface; the value is the light theme's own `--raised`, named here once. A printed
   card is on paper in a dark room too.

   The `aspect-ratio` comes from the `width` and `height` the API serves off the source,
   so the box is the right shape before the picture loads and the step does not jump. */
.deployment-map {
  margin: var(--gap) 0 0;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: #f7f5f2;
  display: block;
}
.deployment-map img {
  display: block;
  width: 100%;
  height: auto;
  max-width: 520px;
  margin: 0 auto;
}
/* On the mission deck the six rows are a list, so the picture sits under its name
   rather than beside it: at 411px a map next to a name is two unreadable columns. */
.detachment-row.wide {
  display: block;
}
.detachment-row.wide > .name { display: block; }

/* **Item 26's button.** Ben: *"a big orange 'ATTACK' button at the bottom of this"*.
   Big is the whole of what is new: the colour is `--fill`, which every primary in this
   app already takes, and this is the one control on the screen so it gets the width and
   a taller tap target rather than a colour of its own. */
/* **The row wraps, which is what makes the button big rather than merely tall.**
   `.roll .actions` is a flex row and its buttons are `flex: 1 1 0`, so ATTACK and Clear
   would share the width evenly and "big" would mean nothing but 52px. Wrapping lets
   ATTACK take the whole first line and Clear sit under it. */
.roll .actions.attack { margin-top: var(--gap); flex-wrap: wrap; }
.roll .actions .primary.big {
  flex: 1 0 100%;
  min-height: 52px;
  font-size: var(--t-lead);
  letter-spacing: var(--track-caps);
}

.roll .parts {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 8px 0 0 30px;
  font-size: var(--t-small);
  color: var(--ink-dim);
}

.roll .parts b { color: var(--ink); font-weight: 700; }
.roll .parts .nested { color: var(--ink-faint); margin-left: 4px; }
.roll .parts .nested b.em { color: var(--accent); }

.roll .step .groups {
  list-style: none;
  margin: 8px 0 0 30px;
  padding: 0;
  font-size: var(--t-small);
  color: var(--ink-dim);
}

.roll .step .groups li {
  display: flex;
  justify-content: space-between;
  padding: 2px 0;
}

.roll .step .tally { margin: 8px 0 0 30px; font-size: var(--t-small); color: var(--ink-dim); }
.roll .step .tally b { color: var(--ink); font-weight: 700; }

.roll .step .note {
  margin: 8px 0 0 30px;
  font-size: var(--t-small);
  color: var(--ink-faint);
}

.roll .step .clause { margin: 8px 0 0 30px; }

.roll .chose,
.roll .unmodelled {
  border-top: 1px solid var(--line);
  padding: 14px 0 0;
}

.roll .chose .what { margin: 0; font-size: var(--t-body); color: var(--ink); }
.roll .chose .why,
.roll .unmodelled .why {
  margin: 6px 0 0;
  font-size: var(--t-small);
  color: var(--ink-dim);
}
.roll .unmodelled .names { margin: 0; font-size: var(--t-body); color: var(--ink); }

.roll .seed,
/* `.roll .empty` and its `no_result` label went with item 26 on 2026-09-23: the setup
   screen IS the empty state now, so a sentence saying nothing had been rolled sat under
   the boxes a reader was about to fill in. */

.roll .actions {
  display: flex;
  gap: 10px;
  margin: var(--gap) 0 0;
}

.roll .actions button {
  flex: 1 1 0;
  min-height: var(--tap);
  border-radius: var(--radius-small);
  font-size: var(--t-body);
  font-weight: 700;
}

.roll .actions .primary {
  background: var(--fill);
  color: var(--onfill);
  border: 1px solid var(--fill);
}

.roll .actions .secondary {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--line);
}

.roll .picker .search {
  width: 100%;
  min-height: var(--tap);
  background: var(--inset);
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  color: var(--ink);
  font-size: var(--t-body);
  padding: 0 10px;
  margin: 0 0 10px;
}

.roll .picker .row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: var(--tap);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  color: var(--ink);
  font-size: var(--t-body);
  text-align: left;
  padding: 6px 0;
}

.roll .picker .row .detail { color: var(--ink-faint); font-size: var(--t-small); }
.roll .picker .row .name em { color: var(--ink-faint); font-style: normal; }

.roll .picker .label {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 12px 0 4px;
}

.roll .picker .row.ability { gap: 8px; }
.roll .picker .row.ability .name {
  flex: 1 1 auto;
  background: transparent;
  border: 0;
  color: var(--ink);
  font-size: var(--t-body);
  text-align: left;
  min-height: var(--tap);
}
.roll .picker .row.ability .x {
  flex: 0 0 48px;
  min-height: 32px;
  background: var(--inset);
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  color: var(--ink);
  font-size: var(--t-body);
  text-align: center;
}
.roll .picker .row.ability .x.wide { flex: 0 0 96px; }

/* Item 10, 2026-09-22. The picker's weapon list is the Builder's `.weapons` table now,
   so the only rules here are the ones that make a ROW a control: the table itself, its
   columns and its sideways scroll all come from the shared block above. A second copy of
   those column widths is how two screens end up with two ideas of a weapon table. */
.roll .picker .weapons tbody tr { cursor: pointer; }
.roll .picker .weapons th[scope="row"] .name {
  background: transparent;
  border: 0;
  padding: 0;
  color: var(--ink);
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.roll .picker .weapons th[scope="row"] .name em {
  color: var(--ink-faint);
  font-style: normal;
  font-weight: 400;
}

/* --------------------------------------------------------------------------
   The recommended terrain layouts, 2026-09-22.

   Ben: "the map layout images ... are from GW directly so I would like to
   actually have those images in the app." Fifteen pairings, each with three
   full-height diagrams, so the pairings are collapsed and the one you rolled is
   the one you open.
   -------------------------------------------------------------------------- */

.layouts .setting {
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px;
  margin: 0 0 var(--gap);
}

.layouts .setting .row {
  display: flex;
  gap: 10px;
  margin: 0 0 8px;
  font-size: var(--t-small);
}

.layouts .setting .label {
  flex: 0 0 84px;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
  padding-top: 1px;
}

.layouts .setting .value { flex: 1 1 auto; color: var(--ink-dim); }

.layouts .footprints {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  list-style: none;
  margin: 4px 0 0 94px;
  padding: 0;
  font-size: var(--t-small);
  color: var(--ink-dim);
}

.layouts .footprints b { color: var(--ink); font-weight: 700; }

.layouts .pick {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 8px;
}

.layouts .pairing { border-top: 1px solid var(--line); }

.layouts .pairing .head {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: var(--tap);
  background: transparent;
  border: 0;
  color: var(--ink);
  font-size: var(--t-body);
  text-align: left;
  padding: 8px 0;
}

.layouts .pairing .names { flex: 1 1 auto; }
.layouts .pairing .names b { font-weight: 700; }
.layouts .pairing .names i { color: var(--ink-faint); font-style: normal; margin: 0 4px; }

/* Items 7 and 8, 2026-09-22. The three letters used to be a hint printed on a
   collapsible head (`.letters`, removed with it); they are the control now, and the
   diagram opens in a `Dialog` rather than expanding under the row.

   Three across on any screen, not `auto-fill`: a letter is one character, so the floor
   the mission deck's tiles need does not apply, and A, B and C on one line is what Ben
   asked for. */
.layouts .layout-tiles {
  grid-template-columns: repeat(3, 1fr);
  margin: 0 0 10px;
}

/* Item 6's mark. One colour here and not two: the companion's pairings are unordered,
   so both drop-downs answer to a single row and there is no opponent's to mark. Faded
   by ink, never `opacity`, and every row stays open. */
.layouts .pairing.is-yours { border-top-color: var(--good); }
.layouts .pairing.is-yours .names b { color: var(--good); }
.layouts .pairing.is-faded .names b,
.layouts .pairing.is-faded .names { color: var(--ink-dim); }
.layouts .pairing .head .tile-mark { flex: 0 0 auto; color: var(--good); }

.layouts figure { margin: 0 0 16px; }

.layouts figcaption {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 0 0 6px;
}

.layouts figcaption .letter {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--accent);
}

.layouts figcaption .missions { font-size: var(--t-small); color: var(--ink-dim); }

/* The diagram itself. A white page on a dark screen, so it sits on its own card
   rather than bleeding into the background. */
.layouts figure img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  background: #ffffff;
}

.layouts figure .missing {
  margin: 0;
  padding: 16px;
  border: 1px dashed var(--line);
  border-radius: var(--radius-small);
  font-size: var(--t-small);
  color: var(--ink-faint);
}

.layouts .credit {
  margin: var(--gap) 0 0;
  font-size: var(--t-small);
  color: var(--ink-faint);
}

/* --------------------------------------------------------------------------
   Play: setting a game up, 2026-09-22.

   The eleven Chapter Approved steps in one flow. The step you are on is open
   and the ones behind it are collapsed to their title and their answer, so
   scrolling back shows what was decided rather than re-asking it.
   -------------------------------------------------------------------------- */

.play .step {
  border-top: 1px solid var(--line);
  padding: 12px 0;
}

.play .step header {
  display: flex;
  align-items: center;
  gap: 10px;
}

.play .step .num {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  border: 1px solid var(--line);
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--t-micro);
  color: var(--ink-dim);
}

.play .step h3 {
  flex: 1 1 auto;
  margin: 0;
  font-size: var(--t-body);
  font-weight: 700;
  color: var(--ink-faint);
}

.play .step.is-current h3 { font-size: var(--t-lead); color: var(--ink); }
.play .step.is-current .num { border-color: var(--accent); color: var(--accent); }
.play .step.is-done .num { color: var(--ink-faint); }

.play .step .derived-right {
  flex: 0 0 auto;
  font-size: var(--t-small);
  font-weight: 700;
  color: var(--accent);
}

.play .step .message { margin: 6px 0 0 30px; font-size: var(--t-small); color: var(--ink-dim); }
.play .step .note { margin: 8px 0 0 30px; font-size: var(--t-small); color: var(--ink-faint); }

.play .sides { margin: 10px 0 0 30px; }

.play .side {
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  padding: 10px;
  margin: 0 0 8px;
}

.play .side > .label,
.play .step .label {
  display: block;
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 4px;
}

.play .field { display: block; margin: 0 0 8px; }

.play .field input {
  width: 100%;
  min-height: var(--tap);
  background: var(--inset);
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  color: var(--ink);
  font-size: var(--t-body);
  padding: 0 10px;
}

.play .chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 8px;
}

.play .derived { margin: 8px 0 0; font-size: var(--t-body); }
.play .derived b { color: var(--accent); font-weight: 700; }

.play .dealt {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 10px 0 0 30px;
  font-size: var(--t-body);
}

.play .dealt b { color: var(--accent); font-weight: 700; font-size: var(--t-lead); }
.play .dealt .label { margin: 0; }

/* The hand-over. The only screen in this app that deliberately shows nothing. */
.play .handover {
  margin: 10px 0 0 30px;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  padding: 12px;
}

.play .handover .prompt {
  margin: 0;
  font-size: var(--t-lead);
  font-weight: 700;
  color: var(--ink);
}

.play .handover .why {
  margin: 4px 0 10px;
  font-size: var(--t-small);
  color: var(--ink-dim);
}

/* The hand-over's OWN buttons, not every button inside it. Written as a descendant
   first, which caught the five Force Disposition chips nested two levels down and drew
   them as five full-width stacked blocks. */
.play .handover > button {
  width: 100%;
  min-height: var(--tap);
  border-radius: var(--radius-small);
  font-size: var(--t-body);
  font-weight: 700;
  margin: 8px 0 0;
}

.play .handover > .primary { background: var(--fill); color: var(--onfill); border: 1px solid var(--fill); }
.play .handover > .secondary { background: transparent; color: var(--ink); border: 1px solid var(--line); }

.play .rolloff { margin: 10px 0 0 30px; }
.play .rolloff .pair { display: flex; gap: 8px; margin: 0 0 6px; }
.play .rolloff .won { margin: 4px 0 0; font-size: var(--t-body); font-weight: 700; color: var(--accent); }

.play .layouts-inline {
  display: flex;
  gap: 8px;
  margin: 0 0 8px;
}

.play .layouts-inline figure {
  flex: 1 1 0;
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  padding: 4px;
  background: transparent;
}

.play .layouts-inline figure.is-on { border-color: var(--accent); }

.play .layouts-inline figcaption {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  color: var(--ink-faint);
  text-align: center;
  margin: 0 0 4px;
}

.play .layouts-inline figure.is-on figcaption { color: var(--accent); }

.play .layouts-inline img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 4px;
  background: #ffffff;
}

.play .blocked {
  margin: 10px 0 0 30px;
  font-size: var(--t-small);
  color: var(--bad);
}

.play .blocked .label { display: inline; color: var(--bad); margin-right: 6px; }

/*
  Item 20b's capacity verdict, and `P5` decides which of the four lines gets colour:
  emphasis is reserved for an outcome that CHANGES the result. Over capacity and a
  refused passenger are both illegal formations and both block the step, so both take
  `--bad` in the same weight the blocking list does. A capacity that fits, and the clause
  the app set aside, are notes: the first because "fits" is the ordinary answer and the
  second because a caveat coloured like an error reads as one, which would tell a player
  something is wrong with a list that is fine.
*/
.play .formation [data-role="capacity-verdict"][data-fits="over"],
.play .formation [data-role="capacity-refused"] { color: var(--bad); }

.play .actions { display: flex; gap: 10px; margin: var(--gap) 0 0; }

.play .actions button {
  flex: 1 1 0;
  min-height: var(--tap);
  border-radius: var(--radius-small);
  font-size: var(--t-body);
  font-weight: 700;
}

.play .actions .primary { background: var(--fill); color: var(--onfill); border: 1px solid var(--fill); }
.play .actions .primary[disabled] { background: var(--inset); color: var(--disabled-ink); border-color: var(--line); }
.play .actions .secondary { background: transparent; color: var(--ink); border: 1px solid var(--line); }
.play .actions .secondary[disabled] { color: var(--disabled-ink); }

.play .seed {
  display: flex;
  justify-content: space-between;
  margin: var(--gap) 0 0;
  font-size: var(--t-small);
  color: var(--ink-faint);
}

.play .seed .link {
  background: transparent;
  border: 0;
  color: var(--ink-dim);
  font-size: var(--t-small);
  text-decoration: underline dotted;
}

/* ------------------------------------------------------------------ the game

   The scorepad. Ben, 2026-09-22: one phone, and **warn never refuse**, so nothing on
   this screen is drawn as disabled except Next at the end of the fifth battle round,
   which is not a rule being enforced. A warning is a panel that appears, never a
   control that stops working.

   `.game .side > button` and not `.game .side button`, which is the selector shipment
   22 got wrong one file over: written as a descendant it caught the chips two levels
   inside and drew five stacked blocks where a row was meant. */

.game .masthead .where {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: baseline;
  margin: 4px 0 0;
  font-size: var(--t-small);
  color: var(--ink-dim);
  letter-spacing: 0.04em;
}

.game .masthead .where em { color: var(--accent); font-style: normal; }

.game .warned {
  margin: var(--gap) 0 0;
  padding: 10px 12px;
  border: 1px solid var(--bad);
  border-radius: var(--radius-small);
  background: var(--inset);
}

.game .warned h3 { margin: 0 0 6px; font-size: var(--t-small); color: var(--bad); }
.game .warned p { margin: 4px 0; font-size: var(--t-small); color: var(--ink); }

.game .side {
  margin: var(--gap) 0 0;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  background: var(--raised);
}

/* Whose turn it is, said with an edge rather than a fill: `P7` reserves emphasis for
   an outcome that changes the result, and whose turn it is changes every rule on the
   screen under it. */
.game .side.is-turn { border-color: var(--accent); }

.game .side > header {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: baseline;
  justify-content: space-between;
}

.game .side > header h2 { margin: 0; font-size: var(--t-body); }

.game .tally { display: flex; gap: 5px; align-items: baseline; margin: 0; }
.game .tally b { font-size: var(--t-display); font-variant-numeric: tabular-nums; }
.game .tally span { font-size: var(--t-small); color: var(--ink-dim); }
.game .tally.is-warned b { color: var(--bad); }

.game .side h3 {
  margin: var(--gap) 0 6px;
  font-size: var(--t-small);
  color: var(--ink-dim);
  letter-spacing: 0.06em;
}

.game .sub { margin: 4px 0; font-size: var(--t-small); color: var(--ink-faint); }

.game .card {
  margin: 8px 0;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  background: var(--inset);
}

.game .card h4 { margin: 0; font-size: var(--t-body); letter-spacing: 0.04em; }
.game .card h5 { margin: 8px 0 2px; font-size: var(--t-small); color: var(--ink-dim); }
.game .card .intro,
.game .card .when { margin: 4px 0; font-size: var(--t-small); color: var(--ink-dim); }

.game .card .score {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: baseline;
  margin: 6px 0;
  font-size: var(--t-small);
}

.game .card .score > span { flex: 1 1 60%; }
.game .card .score > i { color: var(--ink); font-style: normal; font-weight: 700; }

/* The VP is a tap target, so it is sized as one. A score line is the thing a player
   reaches for mid-game with a model in the other hand. */
.game .card .score .link,
.game .cards .link {
  min-height: var(--tap);
  min-width: var(--tap);
  padding: 0 10px;
  border: 1px solid var(--edge);
  border-radius: var(--radius-small);
  background: transparent;
  color: var(--accent);
  font-size: var(--t-body);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.game .card .score .link i { font-size: var(--t-small); font-weight: 400; color: var(--ink-dim); }

.game .cards { margin: 0; padding: 0; list-style: none; }

.game .cards li {
  display: flex;
  gap: 10px;
  align-items: center;
  justify-content: space-between;
  padding: 6px 0;
  border-bottom: 1px solid var(--line);
  font-size: var(--t-small);
}

.game .cards li > span { flex: 1 1 auto; }
.game .cards li i { display: block; color: var(--ink-faint); font-style: normal; }

.game .side .row { display: flex; gap: 8px; margin: 8px 0 0; }

.game .side .row > button {
  flex: 1 1 0;
  min-height: var(--tap);
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  background: transparent;
  color: var(--ink);
  font-size: var(--t-small);
}

.game:not(.phase-screen) .field { display: block; margin: 8px 0 0; }
.game:not(.phase-screen) .field span { display: block; font-size: var(--t-small); color: var(--ink-dim); }

.game:not(.phase-screen) .field input {
  width: 100%;
  min-height: var(--tap);
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  background: var(--ground);
  color: var(--ink);
  font: inherit;
}

.game .ledger { margin: var(--gap) 0 0; }
.game .ledger h3 { font-size: var(--t-small); color: var(--ink-dim); }
.game .ledger ul { margin: 0; padding: 0; list-style: none; }

.game .ledger li {
  display: flex;
  gap: 8px;
  justify-content: space-between;
  padding: 5px 0;
  border-bottom: 1px solid var(--line);
  font-size: var(--t-small);
  color: var(--ink-dim);
}

.game .ledger li b { color: var(--ink); font-variant-numeric: tabular-nums; }

/* The two numbers this screen enforces against a source weaker than a clause. On the
   screen rather than in `GAPS.md` alone, because `C5a`: an instrument's exclusions are
   part of its answer. */
.game .declared { margin: var(--gap) 0 0; }

.game .declared p {
  margin: 6px 0;
  font-size: var(--t-small);
  color: var(--ink-faint);
  border-left: 2px solid var(--line);
  padding-left: 10px;
}

.game .actions { display: flex; gap: 10px; margin: var(--gap) 0 0; align-items: center; }

.game .actions .primary {
  flex: 1 1 auto;
  min-height: var(--tap);
  border: 1px solid var(--fill);
  border-radius: var(--radius-small);
  background: var(--fill);
  color: var(--onfill);
  font-size: var(--t-body);
  font-weight: 700;
}

.game .actions .primary[disabled] {
  background: var(--inset);
  color: var(--disabled-ink);
  border-color: var(--line);
}

.game .actions .link {
  background: transparent;
  border: 0;
  color: var(--ink-dim);
  font-size: var(--t-small);
  text-decoration: underline dotted;
}

/* The sentence a warning is held against, where the warning needs one. Set apart so a
   reader can see which words are the publisher's and which are this app's. */
.game .warned q {
  display: block;
  margin: 4px 0 0;
  color: var(--ink-dim);
  font-style: italic;
}

/* The one block on a card that is asking its question right now.

   `P7`: emphasis is reserved for an outcome that changes the result, and which line is
   live is the only thing on this screen that decides whether a player scores. Nothing
   is dimmed to say it: the other blocks stay at full strength, because a player reads
   the whole card to plan the next turn. */
.game .card .block.is-live {
  margin: 8px -10px;
  padding: 8px 10px;
  border-left: 3px solid var(--accent);
  background: var(--raised);
}

/* 1.5.1: the heading in full ink. K1: "A heading, a number, a badge or a title you can
   only read is never orange"; the live block is the current state (E1), so its edge is
   orange and its words are not. It was never on an audited screen until the other
   player's End of Turn cards (DESIGN.md 26.9) put one there. */
.game .card .block.is-live h5 { color: var(--ink); }

/* The winner's name is already under the dice in the accent, so this is the verb and
   not a second copy of it. Sentence case deliberately: `.label` is uppercase and a
   whole sentence in capitals is a shout. */
.play .chooser {
  margin: 4px 0 0 30px;
  font-size: var(--t-small);
  color: var(--ink-dim);
}

/* Ben's item 15: Next and Back sit in the banner rather than below eleven steps. */
.play .masthead { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.play .masthead .actions { display: flex; gap: 8px; margin: 0; flex: 0 0 auto; }

.play .masthead .actions button {
  min-height: var(--tap);
  padding: 0 12px;
  border-radius: var(--radius-small);
  font-size: var(--t-small);
  font-weight: 700;
  /* "Back a step" wrapped to two lines at 375px in the first pass. The label is served
     and stays as it is: it has to read as a step in the flow rather than as the app's
     own Back, which now floats in the corner beside it. */
  white-space: nowrap;
}

.play .masthead h1 { min-width: 0; overflow-wrap: break-word; }

/* Ben's item 27: the minus and the tally, inside the scoring line they belong to.
   The VP button is the plus, so there is no separate one: two controls doing one thing
   is the defect the welcome screen was rebuilt to remove, at a smaller scale. */
.game .card .score .minus {
  min-width: var(--tap);
  border-color: var(--line);
  color: var(--ink-dim);
}

.game .card .score .tally {
  min-width: 1.5em;
  text-align: center;
  font-size: var(--t-body);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/*
  **Play's first question, 2026-09-25.** Ben: *"on the 'Play' section can you please have
  a 'create new game' and 'resume game' button as the initial prompt?"* Two full-width
  buttons, stacked, because they are the only two things the screen asks and both are
  long phrases: side by side at 411px, "Create new game" wraps.
*/
.play-home .actions,
.resume .actions {
  margin: 12px 0 8px;
  display: grid;
  gap: 8px;
}
.play-home .actions .primary,
.resume .actions .primary {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--tap);
  text-align: center;
  text-decoration: none;
}
.play-home .actions .count { font-weight: 400; font-size: var(--t-small); }
.play-home .abilities { margin-top: 12px; }

/* The box that asks before a new game replaces one in progress: two choices, stacked. */
.actions.stacked { display: grid; gap: 8px; }

/* A game in progress on the Resume screen: a card like a saved list, without the colour. */
.held-game {
  display: block;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--raised);
  margin-bottom: 8px;
  padding: 0 0 8px;
}
.held-game .card-top {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px 4px 12px;
  min-height: var(--tap);
}
.held-game .card-top .name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--t-lead);
  font-weight: 600;
}
.held-game .card-top .chip { display: inline-flex; align-items: center; text-decoration: none; }
.held-game > .wnotes { display: block; padding: 0 12px; }
.chooser .held { color: var(--accent); }

/* **Combat Patrol in New army, 2026-09-25.** A patrol's name and its Add button on one
   line, its units underneath. */
.patrol-choice .card-top { display: flex; align-items: center; gap: 8px; }
.patrol-choice .card-top .name { flex: 1 1 auto; min-width: 0; }
.patrol-choice .card-top .chip { flex: 0 0 auto; }

/* The two buttons on Play that throw a game away keep the danger colour. `.play .actions
   .primary` above paints every primary on a Play screen with the fill, and without this it
   painted Discard the same orange as Resume, which is the one pair that must not match. */
.play .actions .primary.danger { background: var(--bad); border-color: var(--bad); }

/* **Settings, 2026-09-25.** The phone build's version in the very top right, as Ben
   asked; the rules corpus version stays in About, now labelled as that. */
.settings-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.settings-head .release { font-size: var(--t-small); color: var(--ink-dim); white-space: nowrap; }
/* Outlined, not filled: the accent fill is for the one thing a screen is for, and
   Settings is for reading and changing settings, not for sending or giving. */
.settings-screen .setting > .primary {
  margin-top: 8px;
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--edge);
}
.feedback-form textarea {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: var(--t-body);
  color: var(--ink);
  background: var(--inset);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  padding: 10px;
  resize: vertical;
}
.feedback-form .actions, .donate-amounts + .actions { margin-top: 12px; }
.donate-amounts { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 10px; }
.donate-amounts .chip { min-height: var(--tap); font-size: var(--t-body); }

/* The faction's colour as a small square, right of the name (Ben, 2026-09-25). A thin
   edge so the darkest publisher colours still read on the dark ground. */
.line .swatch {
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  margin-left: auto;
  border-radius: 3px;
  box-shadow: 0 0 0 1px var(--edge);
}

/* **A rule as a row that opens a pop-up**, 2026-09-25 (`RuleDetails` in `ui.js`). It
   takes the place the folded `<details>` did, reads as the same kind of row as the
   rest of its list, and says it opens something with the same chevron. */
.rule-pop-row { border-bottom: 1px solid var(--line); }
.rule-pop-row:last-child { border-bottom: 0; }
.rule-open {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  min-height: var(--tap);
  padding: 8px 0;
  background: none;
  border: 0;
  color: var(--ink);
  font: inherit;
  font-size: var(--t-lead);
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.rule-open::after {
  content: "";
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  border-right: 2px solid var(--ink-dim);
  border-bottom: 2px solid var(--ink-dim);
  transform: rotate(-45deg);
  margin-right: 4px;
}
.rule-pop-body > .message + .message { margin-top: 8px; }
.rule-pop .masthead { position: static; }

/*
  **Combat Patrols, spaced out, 2026-09-25.** Ben: "Can you space out the combat patrol
  rules a bit? They're very cramped." Every section of the index and of a patrol's page
  gets the gap a section gets elsewhere, a rule inside a section the gap a paragraph gets,
  and each patrol row stacks its source under its name rather than beside it.
*/
.patrols > section, .patrols > details, .patrols > h2,
.patrol-page > section, .patrol-page > details { margin-top: 22px; }
.patrols .ability + .ability, .patrol-page .ability + .ability { margin-top: 14px; }
.patrols .detachment-row.wide, .patrol-page .detachment-row.wide { padding: 10px 0; }
.patrols .detachment-row.wide > .wnotes { margin-top: 2px; }
.patrol-page .message + .message, .patrols .message + .message { margin-top: 8px; }
.patrol-page .line, .patrol-page .who { margin-top: 10px; }

/* The faction's colour on a Combat Patrol faction heading: a bar on the left and a tint
   behind the name, so a scroll through 28 factions reads by colour before by word. */
.faction-head[style] {
  padding: 6px 10px;
  border-left: 4px solid var(--faction-colour);
  background: color-mix(in srgb, var(--faction-colour) 24%, transparent);
  border-radius: 0 var(--radius-small, 4px) var(--radius-small, 4px) 0;
  color: var(--ink);
}

/*
  **The Core Rules sections at the index's size, 2026-09-25.** Ben: "Under core rules:
  Can we increase the font size so it is similar to the faction rules/datasheets fonts?
  Are they all caps here because they are in the core rules?"

  They were not: the book's section titles are "Core Concepts", "Moving" and so on, and
  the caps were this stylesheet's label treatment (11px, tracked, uppercase), put on the
  fold headers on 2026-09-15 to tell a header from its contents. The Faction Rules and
  Unit Datasheets indexes draw their rows at `--t-lead` 600 as the book's own words, and
  this does the same, one weight heavier, so a section still reads above the clauses that
  open under it. The provenance fold (`.aside`) keeps the label treatment: it is the app
  talking, not the book.
*/
.section-group > summary {
  font-size: var(--t-lead);
  font-weight: 700;
  letter-spacing: 0;
  text-transform: none;
}
.section-group > summary .points { font-size: var(--t-small); }

/* **Who can lead this unit, open, 2026-09-25** (it was closed by default under F7). The
   publisher's sentence as a heading at the size a fold header had, the names under it. */
.led-by { margin-top: 12px; }
.led-by > .led-head {
  margin: 0 0 4px;
  color: var(--ink);
  font-size: var(--t-body);
  font-weight: 600;
}
.row .value.chips { display: flex; flex-wrap: wrap; gap: 6px; }

/* ==========================================================================
   **THE TYPE RULES, written down, 2026-09-25.** Ben: "Can you also review all fonts
   across the app again? Particularly in the dataslate. I feel like things aren't as
   consistent as they could be. We should write some rules to adhere to for all screens
   we create." The rules are `TYPOGRAPHY.md` at the root of the project; these are the
   places a screen was breaking them, measured over sixteen screens.
   ========================================================================== */

/* T4 and T10: rules prose is --t-body wherever it lands. A `.message` with no surface
   rule took the browser's 16px: every Combat Patrol step and mission, and the text in
   the Settings boxes. `:where` keeps this at zero specificity, so every scoped rule
   that already sets a size still wins. */
:where(.message) { font-size: var(--t-body); line-height: 1.5; }

/* T5: one rule opened on its own is read, not scanned, and takes the reading size the
   Core Rules rule screen has always had. */
.rule-pop-body .message { font-size: var(--t-lead); line-height: 1.5; }

/* T6: a section label is the one label treatment, on the datasheet as everywhere else.
   It was 12px 600 in sentence case here and 11px 700 caps on every other screen, so the
   datasheet's "Abilities" and the Faction Rules page's "ARMY RULES" were the same role in
   two voices. */
.sheet-read .label {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 6px;
}

/* T8: a rule's name after the paragraph before it gets a gap, so a run of named rules
   (an army rule's entries, a patrol's enhancements) reads as a list of rules rather than
   one block. The Faction Rules page had Skullsquirm Blight butting its own heading. */
.message + .who, .wnotes + .who, .message + .ability, .ability + .ability { margin-top: 12px; }
/* T10, the rest the measurement found: text on a Combat Patrol page, the rules corpus
   version on Settings and the empty Forces line, all at the browser's 16px. */
.patrol-page .masthead .detachment { font-size: var(--t-small); color: var(--ink-dim); }
.patrol-page p.who { font-size: var(--t-body); font-weight: 600; }
.patrol-page p.who .badge { font-size: var(--t-small); }
.settings-screen [data-role="app-version"] .points { font-size: var(--t-body); }
:where(p.loading) { font-size: var(--t-body); }
.patrol-page p.line .name { font-size: var(--t-body); font-weight: 600; }

/* ======================================================================================
   THE KIT. DESIGN.md, locked 2026-09-25.

   Ben: "make everything we build going forward use these elements strictly", then "let's
   lockit in and apply it all."

   Every element DESIGN.md defines is drawn here, once, and nowhere else. The rules above
   this block were written screen by screen over five weeks; they still lay things out
   (widths, gaps, grids), but how a control LOOKS is decided here.

   **Why every selector starts with `#app`.** The screen rules above reach three and four
   classes deep (`.play .actions .primary.danger`), and a kit that lost to them would be a
   kit in name only. One id outranks any number of classes, so a kit rule wins over every
   screen rule without `!important`, and the screen rules keep the properties the kit
   does not set. Every dialog, the tab bar and Back render inside `#app`.

   `tests/design_audit.py` measures every screen against DESIGN.md; the section numbers
   below are DESIGN.md's.
   ====================================================================================== */

/* Text that no rule sizes is 13px, not the browser's 16 (T10). `html` keeps 16px because
   it is the rem the five tokens are measured in. */
body { font-size: var(--t-body); }
#app { padding-bottom: calc(var(--back-lift) + var(--back-h) + 16px); }

/* --- 5. Buttons ----------------------------------------------------------------------- */

#app :is(button, input, select, textarea) { font-family: inherit; }
#app button { -webkit-tap-highlight-color: transparent; }

/* B1 primary */
#app .primary {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 48px;
  padding: 0 16px;
  border: 1px solid var(--fill);
  border-radius: var(--radius-small);
  background: var(--fill);
  color: var(--onfill);
  font-size: var(--t-lead);
  font-weight: 700;
  line-height: 1.25;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
#app .primary .count { font-size: var(--t-small); font-weight: 600; color: var(--onfill); }

/* B2 secondary */
#app .secondary {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 48px;
  padding: 0 16px;
  border: 1px solid var(--edge);
  border-radius: var(--radius-small);
  background: transparent;
  color: var(--ink);
  font-size: var(--t-lead);
  font-weight: 600;
  line-height: 1.25;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}

/* B4 danger: the red button, inside a confirm prompt only. */
#app .primary.danger {
  border-color: var(--bad);
  background: var(--bad);
  color: var(--on-bad);
}

/* B5 action chip: a verb on a row. 36px drawn, 44px touched. `button.link` was a text
   button, which DESIGN.md does not have; every one of them was a verb on a row. */
#app :is(.chip, button.link) {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 36px;
  padding: 0 12px;
  border: 1px solid var(--edge);
  border-radius: var(--radius-small);
  background: transparent;
  color: var(--ink);
  font-size: var(--t-body);
  /* 500, B5 as written. Ben, 2026-09-25: "Allow 500", the fourth weight on the scale,
     for chips alone. */
  font-weight: 500;
  line-height: 1.25;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}
#app :is(.chip, button.link)::after { content: ""; position: absolute; inset: -4px -2px; }
/* A toggle chip that is off: dim, so the chosen one reads (E1). */
#app .chip[aria-pressed="false"] { color: var(--ink-dim); }

/* B6 destroy chip: starts a delete, never finishes one. */
#app :is(.chip.remove, button.link.remove, button.danger:not(.primary)) {
  border: 1px solid color-mix(in srgb, var(--bad) 55%, var(--line));
  background: transparent;
  color: var(--bad);
}
#app button.danger:not(.primary) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 36px;
  padding: 0 12px;
  border-radius: var(--radius-small);
  font-size: var(--t-body);
  font-weight: 400;
}

/* B3 disabled: not yet. Dashed, which nothing else in the app is. */
#app button:disabled,
#app button:disabled:is(.primary, .secondary, .chip, .link) {
  border: 1px dashed var(--line);
  background: var(--inset);
  color: var(--ink-faint);
  font-weight: 600;
  cursor: default;
}
#app button.chip:disabled, #app button.link:disabled { font-weight: 500; }

/* B7 pressed: darker fills, inset under outlines. Instant in, 120ms out. */
#app :is(.primary, .secondary, .chip, button.link, .tile-name, .detachment-row > a.name,
         .lines .line > a, .rule-pop-row > button) {
  transition: background-color 120ms ease-out;
}
#app .primary:not(:disabled):active {
  background: color-mix(in srgb, var(--fill) 82%, #000);
  transition-duration: 0ms;
}
#app .primary.danger:not(:disabled):active { background: color-mix(in srgb, var(--bad) 82%, #000); }
#app :is(.secondary, .chip, button.link, .tile-name):not(:disabled):active {
  background: var(--inset);
  transition-duration: 0ms;
}

/* --- 6. Links and small controls -------------------------------------------------------- */

/* L3 reference chip */
#app .clause {
  display: inline-block;
  padding: 0 5px;
  border: 1px solid var(--accent);
  border-radius: 5px;
  color: var(--accent);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-micro);
  font-weight: 400;
  letter-spacing: 0;
  text-decoration: none;
  vertical-align: 1px;
}

/* L1 inline link: a word inside text that opens something. */
#app :is(.message, .wnotes, .note, .who, .rules-text, p) a[href]:not(.clause):not(.primary):not(.chip):not(.secondary),
#app a.inline {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent) 50%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* L7 no white underlines, and L2 a row that opens something carries a chevron. A row's
   name is ink (Ben, 2026-09-22) and the whole row is the target. */
#app :is(.detachment-row > a.name, .detachment-row > .name.explains) {
  color: var(--ink);
  text-decoration: none;
}
/* A stacked row (name over its note): the chevron ends the name's line. */
#app .detachment-row.wide > :is(a.name, .name.explains) {
  display: flex;
  align-items: center;
  gap: 8px;
}
#app .detachment-row.wide > :is(a.name, .name.explains)::after {
  content: "\203A";
  margin-left: auto;
  flex: none;
  color: var(--ink-faint);
  font-size: var(--t-lead);
  font-weight: 400;
}
/* A one-line row (name, then its count or note): the chevron ends the row. */
#app .detachment-row:not(.wide):has(> a.name, > .name.explains)::after {
  content: "\203A";
  flex: none;
  margin-left: 8px;
  color: var(--ink-faint);
  font-size: var(--t-lead);
  font-weight: 400;
}

/* --- 8. Pop-up boxes --------------------------------------------------------------------- */

/* L4: the close X is an orange glyph on a 44px target, not a boxed chip (Ben's G1). */
#app .dialog-head .close {
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0;
  border: 0;
  background: transparent;
  box-shadow: none;
  color: var(--accent);
  font-size: var(--t-lead);
}
#app .dialog-head .close::after { content: none; }

#app .dialog-head :is(h2, .section) {
  margin: 0;
  color: var(--ink);
  font-size: var(--t-lead);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: 0;
}

/* --- 10. Selected ------------------------------------------------------------------------ */

/* E1: 2px orange, drawn as a border and an inset ring so nothing moves when it turns on. */
#app :is(.chip.on, .chip[aria-pressed="true"], .tile.on, .det.on) {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
  color: var(--ink);
}
#app .mission-grid tr.on { outline: 2px solid var(--accent); outline-offset: -2px; }
#app .mission-grid tr.on td { background: none; }

/* --- 11. Tiles --------------------------------------------------------------------------- */

#app .tile.is-yours { border-color: var(--good); box-shadow: inset 0 0 0 1px var(--good); }
#app .tile.is-theirs { border-color: var(--bad); box-shadow: inset 0 0 0 1px var(--bad); }
/* Both: a green outline and a red outline, each all the way round. Ben, 2026-09-25: "on
   'both' can you do both outlines all the way around?" */
#app .tile.is-both {
  border-color: var(--good);
  box-shadow: inset 0 0 0 1px var(--good), inset 0 0 0 2px var(--raised), inset 0 0 0 4px var(--bad);
}
#app .tile.is-both .tile-mark { color: var(--ink); }

/* --- 12. Forms, 13. number fields ---------------------------------------------------------- */

#app :is(input[type="text"], input[type="search"], input:not([type]), input[inputmode],
         select, textarea) {
  min-height: var(--tap);
  border: 1px solid var(--edge);
  border-radius: var(--radius-small);
  background: var(--inset);
  color: var(--ink);
  font-size: var(--t-lead);
}
#app textarea { min-height: 96px; }
#app :is(input, select, textarea)::placeholder { color: var(--ink-faint); }
/* M3: no focus border. The keyboard and the cursor say which field you are in. */
#app :is(input, select, textarea):focus { outline: none; border-color: var(--edge); box-shadow: none; }
/* M1: a field's label is the capitals label. */
#app :is(label.field > span:first-child, .field > .field-label, .field-row > label > span:first-child) {
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

/* --- 15. Badges and keywords ------------------------------------------------------------- */

/* V2 badge: a fact, which does nothing. */
#app .badge {
  display: inline-block;
  padding: 2px 6px;
  border: 0;
  border-radius: 5px;
  background: var(--inset);
  color: var(--ink-dim);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}
/* V3 keyword: the publisher's word, on the inset ground. */
#app .keyword {
  display: inline-block;
  padding: 2px 6px;
  border: 0;
  border-radius: 5px;
  background: var(--inset);
  color: var(--ink);
  font-size: var(--t-small);
  font-weight: 600;
}

/* --- 4. The frame -------------------------------------------------------------------------- */

/* N3: the tab labels are not capitals, so they take no tracking. */
#app .tabbar .tab { letter-spacing: 0; }

/* --- 16. Game pieces ------------------------------------------------------------------------ */

/* G6 dice. Ben, 2026-09-25: "let's make a critical gold and lets make a success the
   outline and pips in gold." Dice are the one place gold is not a control (K7). */
#app .die.is-on[data-status] { border-color: var(--accent); }
#app .die.is-on[data-status] .pip.on { background: var(--accent); }
#app .die.is-crit { border-color: var(--accent); background: var(--accent); }
#app .die.is-crit .pip.on { background: var(--onfill); }
#app .die .again { background: var(--ink); box-shadow: 0 0 0 2px var(--ground); }

/* --- 17. Faction colours --------------------------------------------------------------------- */

/* H4: a Combat Patrol faction heading is the band: the name in white capitals on its own
   colour. The 4px bar and the tint are retired; the darkest colours vanished in them. */
#app .faction-head[style] {
  padding: 6px 12px;
  border: 0;
  border-radius: 5px;
  background: var(--faction-colour);
  color: var(--on-strat);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

/* --- 14. Loading ------------------------------------------------------------------------------ */

/* Z1: placeholder bars in the shape of what is coming, no words. */
#app .skeleton { display: grid; gap: 10px; padding: 8px 0; }
#app .skeleton > span {
  display: block;
  height: 14px;
  border-radius: 5px;
  background: linear-gradient(90deg, var(--inset), var(--raised), var(--inset));
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.4s linear infinite;
}
#app .skeleton > span:nth-child(1) { width: 55%; }
#app .skeleton > span:nth-child(3) { width: 80%; }
@keyframes skeleton-shimmer { from { background-position: 200% 0; } to { background-position: 0 0; } }
@media (prefers-reduced-motion: reduce) { #app .skeleton > span { animation: none; } }

/* Z4 done: a short line at the foot of the screen. */
.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--back-lift) + var(--back-h) + 12px);
  z-index: 70;
  transform: translateX(-50%);
  max-width: calc(100vw - 32px);
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  background: var(--inset);
  color: var(--ink);
  font-size: var(--t-body);
}

/* --- T4 rules text ------------------------------------------------------------------------------ */

/* Every paragraph of the publisher's prose: 13px, full ink, first line indented. Ben: "Let's
   make all rules text consistent at 13px" and "Make all rules text ink also make all rules
   text start with an indent in the paragraph". A one-line answer (a stratagem's WHEN) and
   the app's own prompts are `.flat`. */
#app .message {
  color: var(--ink);
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.5;
  text-indent: 1.25em;
}
#app :is(.message.flat, .message.step, .finding .message) { text-indent: 0; }
#app .message.step { padding-left: 0; }
/* The app talking in a `.message`, which T9 sets small and dim. */
#app :is(.filtered .message, .message.app) {
  color: var(--ink-dim);
  font-size: var(--t-small);
  text-indent: 0;
}

/* --- Corners (DESIGN.md 1): 10 for anything you press or read a value in, 14 for what
   holds, 5 for badges. The stat boxes, the deployment figures, the formation blocks and
   the Configure button were on 4, 6 and 8. -------------------------------------------- */
#app :is(.statline .stat, figure[data-role="deployment-figure"], .play .formation,
         .deployment-map, .play .step img, .build-row [data-role="edit"]) {
  border-radius: var(--radius-small);
}

/* G2: a stat value sits on the inset ground, so a stat line inside a pop-up is not a
   raised card inside a raised card (S1). */
#app .statline .stat { background: var(--inset); border-color: var(--inset); }

/* T1: the roster's list name is the screen title. */
#app .masthead .list-name { font-size: var(--t-display); letter-spacing: var(--track-display); }

/* `b` and `strong` inside something already bold resolve to 900, which is not a rung. */
#app :is(b, strong) { font-weight: 700; }

/* K1 and G4 to G6: numbers and headings you only read are ink, never orange. */
#app :is(.roll .step .derived .on, .play .step .derived-right, .play .rolloff .won,
         .play .drawn-letter, .mission-card .score .vp, .roll .step .figure b, .roll b) {
  color: var(--ink);
}
#app .mission-card .score .vp { font-size: var(--t-lead); }
#app .mission-card .label { color: var(--ink); }
#app .list-summary .label, #app .list-summary h3.label { color: var(--ink-faint); }
#app .summary-foot .enh .one { color: var(--ink); }
#app .chooser .held { color: var(--ink-dim); }

/* N3: headings in sentence case carry no tracking. */
#app :is(.game h3, .homebrew-game h3, .game h4, .homebrew-game h4, .mission-card .score .pre) {
  letter-spacing: 0;
}

/* S2: a group inside a card is a section under a hairline and a label, not a card inside
   a card. The game's player card held its mission and secondaries as boxed cards. */
#app .game .side h3 {
  margin: var(--gap) 0 6px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}
#app .game .side .card {
  margin: 8px 0 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}
#app .game .side .card + .card { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line); }
#app .game .card h4 { font-size: var(--t-lead); font-weight: 600; letter-spacing: 0; }

/* D2: inside a rule's pop-up the title bar names the rule, so the screen heading is not
   drawn a second time. */
#app .rule-pop .masthead h1 { display: none; }

/* U1: a number field reads as the number. */
#app .roll .fields input,
#app .fields input[data-field] {
  text-align: center;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
#app .fields input[data-field]::placeholder { font-size: var(--t-body); font-weight: 400; }

/* V1 banner: raised, a coloured left edge, the verdict word as a label, then the text. */
#app .banner {
  margin: 0 0 10px;
  padding: 8px 12px;
  border: 0;
  border-left: 3px solid var(--line);
  border-radius: 0 var(--radius-small) var(--radius-small) 0;
  background: var(--raised);
}
#app .banner.is-bad { border-left-color: var(--bad); }
#app .banner-label {
  display: block;
  margin-bottom: 2px;
  color: var(--bad);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}
#app .banner-text { margin: 0; color: var(--ink); font-size: var(--t-body); }
#app .wnotes.empty { margin: 8px 0; }

/* D2 on the unit datasheet box: the title bar names the unit, so the sheet's own heading
   is not drawn a second time inside the box. */
#app .dialog .sheet-head h1 { display: none; }
#app .dialog .sheet-head .title-row { justify-content: flex-end; }

/* The labels inside a rule read on its own (house rules that change it, the HOUSE RULE
   tag) are the app's labels: T6. */
#app .clause-read .label { color: var(--ink-faint); font-size: var(--t-micro); font-weight: 700; }

/* V3: the Warlord word opens its rule, so it is an inline link in the label's capitals. */
#app a[data-role="is-warlord"] {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: var(--accent);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent) 50%, transparent);
  text-underline-offset: 3px;
}

/* The setup screens' masthead: the title on its own line, Back a step and Next step as a
   pair of halves under it (B1, B2). Side by side with the title they squeezed "Set up a
   game" onto three lines once the buttons took their 48px, 15px size. */
#app .play > .masthead { display: grid; grid-template-columns: 1fr; justify-content: stretch; gap: 10px; }
#app .play > .masthead .actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
#app .play > .masthead .actions > button { width: 100%; }

/* A stepper: the number sits between its two chips, centred on them. */
#app .chips:has(> [data-role="setting-number"]) { align-items: center; }
#app [data-role="setting-number"] {
  min-width: 2ch;
  font-size: var(--t-lead);
  font-variant-numeric: tabular-nums;
  text-align: center;
}
/* B2: a secondary alone is full width. */
#app .setting > .secondary { width: 100%; }

/* W6: a space either side of the slash in "990 / 1000". The markup puts the total and
   its cap in two elements, and htm drops the whitespace between them. */
#app .of { margin-left: 0.25em; }

/* M1: every field's label is the capitals label, including the `.key` a field row
   carries (Army, Battle size, Your name). */
#app :is(.field-row > .key, .field-row > label > .key) {
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}
#app .key-with-note { display: inline-flex; align-items: center; gap: 6px; }

/* A pop-up inherits nothing from wherever it was opened. A box rendered inside a label
   (the army's `i` sits on its label) took the label's 11px capitals, faint, bold. */
#app .scrim {
  color: var(--ink);
  font-size: var(--t-body);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
}

/* T3: a rule's name inside a block is 15px 600, a step above its 13px text. Ben, round 2:
   "for rules text, the paragraph heading looks too similar to the rules text itself",
   and option A1. */
#app .ability > .who { color: var(--ink); font-size: var(--t-lead); font-weight: 600; }

/* T4 bullets: the same 13px ink as the paragraph they belong to, not indented. */
#app ul.lines-of-text > li { color: var(--ink); font-size: var(--t-body); font-weight: 400; }

/* T2: the sources under Built on are names in a list, the same on the home screen and in
   Settings (13px 600 on one, 15px 700 on the other until 2026-09-25). */
#app .sources .line .name { font-size: var(--t-lead); font-weight: 600; }

/* The publisher's Designer's Note is kept apart from the rule it clarifies (Ben,
   2026-09-16): inset and dim, still upright because it is still the publisher. */
/* T4: all rules text full ink, the Designer's Note included since Ben's ruling of
   2026-09-25 ("bring it up to full ink"). It is still set apart by its rule and inset. */
#app .message.designer-note { color: var(--ink); }

/* ======================================================================================
   THE COLLECTION, under Forces. COLLECTION_BRIEF.md revision 7, built 2026-09-25.

   Assembled from the kit: cards (R3), toggle chips (E1), the stepper, fields (M2),
   badges (V2), the dim note banner (V1), rows (R1) and tiles for the figures (F1). Five
   elements the kit did not have are drawn here and proposed to Ben in DESIGN.md
   section 25: the photo thumbnail, the header photo band, the framing window, the camera
   view and the full photo. Every value is a token; every selector starts with `#app`
   for the reason the kit's header gives.
   ====================================================================================== */

#app .forces-switch { margin: 0 0 12px; }
#app .collection .hidden-input, #app .hidden-input { display: none; }

/* The figures: tiles on the page (F1), the key as a T6 label, the value as a stat. */
#app .figures { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 0 0 8px; }
#app .figure {
  min-width: 0;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  background: var(--raised);
}
#app .figure > .key {
  display: block;
  margin-bottom: 4px;
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}
#app .figure > b { color: var(--ink); font-size: var(--t-lead); font-weight: 700; font-variant-numeric: tabular-nums; }
#app .figure > .unit, #app .figure > .value-words { color: var(--ink-dim); font-size: var(--t-small); font-weight: 400; }
#app .figure > .value-words { color: var(--ink); font-size: var(--t-body); }

/* A unit's records, under its name as a label with the count right (T12). */
#app .shelf-group { margin: 22px 0 0; }
#app .group-label {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin: 0 0 8px;
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}
#app .group-label > .count { color: var(--ink-dim); font-size: var(--t-small); font-weight: 600; letter-spacing: 0; text-transform: none; }
#app ul.records { list-style: none; margin: 0; padding: 0; }
/* The Collection's faction folds (2026-09-26, DESIGN.md 26.1). A fold (R2) whose header
   is the faction's band (H2): the name in white capitals on the faction's colour, the
   count in the same white at 12px, the chevron white. A faction with no banked colour
   takes the raised colour, since none is invented (H1). */
#app .faction-fold { margin: 22px 0 0; }
#app .faction-fold > summary.faction-band {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  padding: 0 12px;
  border-radius: var(--radius-small);
  background: var(--faction-colour, var(--raised));
  color: var(--on-strat);
}
#app .faction-fold:not([open]) > summary.faction-band { margin-bottom: 0; }
#app .faction-fold > summary.faction-band > .title { flex: 1; }
#app .faction-fold > summary.faction-band > .count {
  font-size: var(--t-small);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
}
#app .faction-fold > summary.faction-band::after { border-color: var(--on-strat); }
#app .faction-fold > summary.faction-band:not([style]) { color: var(--ink); }
#app .faction-fold > summary.faction-band:not([style])::after { border-color: var(--ink-faint); }
#app .faction-fold > .shelf-group:first-of-type { margin-top: 12px; }

/* A record as a tile (F1, 2026-09-26, DESIGN.md 26.2): the model photo square across the
   top where there is one, the name centred under it. Three to a row on a phone. */
#app ul.record-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: 8px;
  list-style: none;
  margin: 0;
  padding: 0;
}
#app .record-tile > .tile-name { justify-content: flex-start; gap: 8px; padding: 8px; }
#app .record-tile .tile-photo {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  background: var(--inset);
}
#app .record-tile .tile-text { font-size: var(--t-body); }


/* A record: a card (R3) whose whole face opens it. */
#app .card.record {
  display: block;
  margin: 0 0 8px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--raised);
  box-shadow: var(--card-shadow);
}
#app .card.record[data-unknown="true"] { border-style: solid; }
#app .record-open {
  display: grid;
  gap: 4px;
  width: 100%;
  padding: 12px 16px;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
#app .record-open:active { background: var(--inset); }
#app li.card.record.wanted-card { display: grid; gap: 4px; padding: 12px 16px; }
#app .wanted-card > .chips { margin: 4px 0 0; }
#app .record-top, #app .record-summary { display: flex; align-items: flex-start; gap: 12px; }
#app .record-name { display: grid; flex: 1; min-width: 0; gap: 2px; }
#app .record-name .name { color: var(--ink); font-size: var(--t-lead); font-weight: 600; overflow-wrap: normal; word-break: normal; }
#app .record-name .record-line { color: var(--ink-dim); font-size: var(--t-small); }
#app .record-summary .record-name p { margin: 0; }
#app .record-count { color: var(--ink); font-size: var(--t-lead); font-weight: 700; font-variant-numeric: tabular-nums; }
#app .stage-words { color: var(--ink); font-size: var(--t-small); font-weight: 600; }
#app .record-notes { color: var(--ink-dim); font-size: var(--t-small); }
#app .record-open > .badge { justify-self: start; }
#app .record-summary { margin: 0 0 4px; }

/* PROPOSED (DESIGN.md 25.1). The model photo: a small square, 10px corners, a hairline. */
#app img.thumb {
  flex: none;
  width: 40px;
  height: 40px;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  object-fit: cover;
}
#app .photo-button { padding: 0; border: 0; background: none; cursor: pointer; }
#app .photo-button > img { display: block; width: 100%; aspect-ratio: 1; border: 1px solid var(--line); border-radius: var(--radius-small); object-fit: cover; }
#app .photo-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
#app img.photo-full { display: block; width: 100%; border-radius: var(--radius-small); }
#app .photo-row { display: flex; align-items: flex-start; gap: 12px; }
#app .photo-row p { margin: 0; }
#app .row-name { margin: 0 0 4px; color: var(--ink); font-size: var(--t-lead); font-weight: 600; }

/* A group inside the record box is a section (S2): a hairline and a T6 label. */
#app .shelf-section { margin: 12px 0 0; padding: 12px 0 0; border-top: 1px solid var(--line); }
#app .shelf-section > h3.label:first-child { margin-top: 0; }
#app .shelf-section > .primary, #app .dialog-body > .primary { width: 100%; margin-top: 14px; }
#app .actions.single { display: flex; justify-content: flex-start; margin: 16px 0 0; }

/* The stepper: the number between its two chips. */
#app .stepper-row { align-items: center; }
#app .step-number { min-width: 2ch; color: var(--ink); font-size: var(--t-lead); font-weight: 700; font-variant-numeric: tabular-nums; text-align: center; }
#app .stepper-row > .key { color: var(--ink-faint); font-size: var(--t-micro); font-weight: 700; letter-spacing: var(--track-caps); text-transform: uppercase; }

/* History, never rewritten: a date and a line, hairlines between. */
#app ul.history { list-style: none; margin: 0; padding: 0; }
#app ul.history > li { display: grid; grid-template-columns: 7.5em 1fr; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: var(--t-body); color: var(--ink); }
#app ul.history .when { color: var(--ink-dim); font-size: var(--t-small); font-variant-numeric: tabular-nums; }

/* The shelf's voice (brief 5.3): the dim note banner, never a finding's colours. */
#app .banner.shelf-note, #app .banner[data-role="backup-reminder"], #app .banner[data-role="storage-status"] { border-left-color: var(--line); }
#app .banner.shelf-note .banner-label { color: var(--ink-dim); }
#app .banner.shelf-note .banner-text { color: var(--ink-dim); font-size: var(--t-small); margin: 2px 0 0; }
#app .banner.shelf-note .shelf-unit + .shelf-unit { margin-top: 8px; }
#app .banner.shelf-note .banner-text b { color: var(--ink); font-weight: 600; }
#app .banner .chips { margin: 8px 0 0; }

/* The Forces tag, a badge under the name (M12 B). */
#app .saved-list .shelf-tag { display: inline-block; margin: 0 12px 8px; }

#app .shelf-storage { margin: 22px 0 0; }
#app .shelf-storage > .primary { width: 100%; margin-top: 8px; }

/* PROPOSED (DESIGN.md 25.3, rebuilt 2026-09-26). The framing window: the saved shape, the
   photo drawn inside it at any zoom from the whole photo to four times the fill. Where the
   photo does not reach an edge the window's own inset shows, and is saved clear. The page's
   camera view that followed this rule until 1.5 is gone: Take a photo opens the phone's
   camera app. */
#app .frame-window {
  position: relative;
  overflow: hidden;
  width: 100%;
  aspect-ratio: 2 / 1;
  margin: 8px 0;
  border: 1px solid var(--edge);
  border-radius: var(--radius-small);
  background-color: var(--inset);
  touch-action: none;
  cursor: grab;
}
#app .frame-photo {
  position: absolute;
  height: auto;
  max-width: none;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* B2 and B6 at full width (2026-09-26): a secondary or a destroy button across the whole
   width, 48px like every button. Delete this list, the unit photo's Replace and Remove
   photo. Ben: "spread the 'replace','remove photo' buttoms across the entire screen" and
   "Spread the 'make a share code' button across the whole section the same as delete this
   list." */
#app :is(button.secondary, button.danger, a.secondary).wide {
  display: flex;
  width: 100%;
  min-height: 48px;
  margin: 8px 0 0;
  font-size: var(--t-lead);
  font-weight: 600;
}

/* Fields in the Collection's boxes: the T6 label above, the field full width (M1, M2). */
#app :is(.shelf-section, .dialog-body) > label.field { display: grid; gap: 4px; margin: 0 0 12px; }
#app :is(.shelf-section, .dialog-body) > label.field > :is(input, textarea, select) { width: 100%; box-sizing: border-box; }
#app .dialog-body > input.search { width: 100%; box-sizing: border-box; margin: 0 0 8px; }

/* The picker's owned count (brief 5.1): the app's small dim words after the name. */
#app .candidate-add .owned-count { display: block; color: var(--ink-dim); font-size: var(--t-small); font-weight: 400; }
#app [data-role="owned-filter-on"] { margin: 0 0 8px; }

/* The Dataslate's owned count (brief 6.2): small dim words, beside the name. */
#app .line > .owned-count, #app .sheet-head h1 > .owned-count {
  margin-left: 8px; color: var(--ink-dim); font-size: var(--t-small); font-weight: 600; letter-spacing: 0;
}
#app .line > .owned-count { margin-left: 0; }

/* PROPOSED (DESIGN.md 25.2). The unit photo behind the datasheet header, fading to the
   sheet's ground at the edges and the foot. The words in the header get a plate drawn
   with a shadow spread, so nothing moves; the stat tiles are already solid. */
#app .sheet-read { position: relative; }
#app .photo-band {
  position: absolute;
  top: -8px;
  left: -16px;
  right: -16px;
  z-index: 0;
  /* Full width from the top, 2026-09-26, rather than `cover`: the band is 2 by 1 on most
     sheets and taller where the header carries more lines, and `cover` answered a taller
     band by enlarging the photo and cutting its sides, which is half of what Ben saw as
     "the photos always present as very zoomed in". What the framing window kept is now
     what the band shows, and any extra height below it is the fade. */
  background-size: 100% auto;
  background-position: center top;
  background-repeat: no-repeat;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent),
                      linear-gradient(to right, transparent, #000 18%, #000 82%, transparent);
  -webkit-mask-composite: source-in;
  mask-image: linear-gradient(to bottom, #000 55%, transparent),
              linear-gradient(to right, transparent, #000 18%, #000 82%, transparent);
  mask-composite: intersect;
}
#app .sheet-read.has-photo > :is(.sheet-top, .sheet-head, .statline-wrap, .model-stats) { position: relative; z-index: 1; }
#app .sheet-read.has-photo .sheet-head :is(h1, .composition, .filtered, .title-row > *) {
  border-radius: 5px;
  background: color-mix(in srgb, var(--raised) 86%, transparent);
  box-shadow: 0 0 0 6px color-mix(in srgb, var(--raised) 86%, transparent);
}
#app .band-preview { display: block; width: 100%; aspect-ratio: 2 / 1; object-fit: cover; border: 1px solid var(--line); border-radius: var(--radius-small); margin: 0 0 8px; }
/* At the foot of Configure since 2026-09-26, so the hairline is above it (S2). */
#app .unit-photo { margin: 16px 0 0; padding: 12px 0 0; border-top: 1px solid var(--line); }
#app .unit-photo > h3.label:first-child { margin-top: 0; }
/* "(x5)" after a unit's name on the roster: the app's small dim words (T9). */
#app .build-row > .model-times { grid-column: 2; grid-row: 1; justify-self: start; align-self: center;
  color: var(--ink-dim); font-size: var(--t-small); font-weight: 600; }

/* A unit photo in Play (brief 6.4, PROPOSED DESIGN.md 25.1): the thumbnail before the
   name, the full photo one tap away. */
#app .formation .line > .photo-button, #app .row > img.thumb { flex: none; margin-right: 8px; }
#app .formation .line > .photo-button > img { width: 40px; height: 40px; aspect-ratio: auto; }
#app .formation .line { display: flex; align-items: center; }
#app .formation .line > .name { flex: 1; }

/* S1: inside a pop-up, a record or a figure is inset, never a second raised card. */
#app .dialog :is(.card.record, .figure) { background: var(--inset); border-color: var(--inset); box-shadow: none; }

/* Two departures the Collection's audit scenes reached first, 2026-09-25: a list that is
   Unfinished (the old roster scene was legal) and a unit with loadout steppers in
   Configure. N3: no tracking on lower-case text. B5 and the corner scale: a stepper is a
   chip, 10px corners, no fill of its own. */
#app .findings-banner > button { letter-spacing: 0; }
#app .findings-banner .n { letter-spacing: 0; }
#app .loadout .step { min-height: 36px; min-width: 36px; border-radius: var(--radius-small); background: transparent; }

/* --- 1.5, Ben's pass of 2026-09-26 ------------------------------------------------------ */

/* The step bar (DESIGN.md 26.3): Previous and Next as halves, floating 12px above the tab
   bar on a ground strip with a hairline, and the app's Back lifted 12px above the strip.
   Ben: "Make next step and previous step floating buttons at the bottom. Move the back
   button above in this instance." */
#app .step-nav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(var(--tabbar) + env(safe-area-inset-bottom, 0px));
  z-index: 55;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  padding: 8px var(--gap);
  border-top: 1px solid var(--line);
  background: var(--ground);
}
#app .step-nav > button { width: 100%; min-height: 48px; }
body:has(.step-nav) {
  --back-lift: calc(var(--tabbar) + env(safe-area-inset-bottom, 0px) + 64px + 12px);
}
#app .has-step-nav { padding-bottom: 136px; }

/* Draw and Roll (26.4): the primary until pressed, then the thing drawn, arriving. The
   dice turn in with the house `die-shake`; a card turns over from its back (`card-draw`,
   DESIGN.md O1 amended 2026-09-26 at Ben's word: "Simple Animations would be amazing too"). */
#app .reveal-button { width: 100%; margin: 4px 0 12px; }
#app .revealed.is-fresh .rolling,
#app .revealed.is-fresh .drawn-letter {
  display: inline-flex;
  animation: die-shake 230ms cubic-bezier(.3, .9, .35, 1) backwards;
  animation-delay: calc(var(--at, 0) * 60ms);
}
#app .revealed.is-fresh :is([data-role="deployment"], .deployment-map, .layouts-inline) {
  animation: card-draw 360ms cubic-bezier(.2, .8, .3, 1) backwards;
  transform-origin: center;
}
@keyframes card-draw {
  from { opacity: 0; transform: perspective(600px) rotateY(80deg) scale(.96); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  #app .revealed.is-fresh :is(.rolling, .drawn-letter, [data-role="deployment"],
                              .deployment-map, .layouts-inline) { animation: none; }
}
#app .play .rolloff .pair { align-items: center; }
#app .play .rolloff .tied { color: var(--ink-dim); font-size: var(--t-small); }

/* The layout in use at the full width, the other two in halves under it (Ben: "Battlefield
   layouts appear too small side by side"). */
#app .play .layouts-inline { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
#app .play .layouts-inline.is-chosen { grid-template-columns: 1fr; }

/* The battle round's screens (DESIGN.md 26.5, 2026-09-26). */
#app .step-nav.single { grid-template-columns: 1fr; }
body:has(.phase-screen) { --back-lift: calc(var(--tabbar) + env(safe-area-inset-bottom, 0px) + 64px + 12px); }
#app .phase-screen { padding-bottom: 136px; }
/* Undo is the Back pill on the right (Ben: "Make 'undo' a floatong button on the right
   the same as 'back'"). Disabled is B3's, with nothing to take back. */
#app .back-float.undo-float { left: auto; right: var(--gap); padding: 0 14px; }
#app .back-float.undo-float:disabled { border-style: dashed; box-shadow: none; }
/* The two floating pills at the top right: Army lists and Stratagems (Ben: "Make
   available strategems a pop up floating box in top right"). The same pill as Back. */
#app .game-floats {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0px) + 10px);
  right: var(--gap);
  /* Under a pop-up's scrim (50), 1.5.1: over it, the two pills covered the box's title. */
  z-index: 45;
  display: flex;
  gap: 8px;
}
#app .float-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  padding: 0 14px;
  border: 1px solid var(--edge);
  border-radius: 999px;
  background: var(--inset);
  color: var(--ink);
  font-size: var(--t-body);
  font-weight: 600;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
  cursor: pointer;
}
#app .float-pill > b { color: var(--ink-dim); font-weight: 600; }
#app .phase-screen > .masthead { padding-top: 52px; }
/* The part's steps: the current one is chosen (E1), the ones behind are dim. */
#app .phase-steps { margin: 12px 0 0; }
#app .phase-steps ol.steps { list-style: none; margin: 4px 0 8px; padding: 0; display: grid; gap: 4px; }
#app .phase-steps li {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 36px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  color: var(--ink-dim);
  font-size: var(--t-body);
}
#app .phase-steps li.is-current { border: 2px solid var(--accent); color: var(--ink); font-weight: 600; }
#app .phase-steps li .num { color: var(--ink-faint); font-variant-numeric: tabular-nums; }
#app .phase-steps li .name { flex: 1; }
#app .draw-prompt { margin: 16px 0 0; padding: 16px; border-radius: var(--radius); background: var(--raised); box-shadow: var(--card-shadow); }
#app .draw-prompt > .primary { width: 100%; margin-top: 8px; }
#app .missions { margin: 16px 0 0; }
#app ul.game-strats { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
/* [ONE SHOT], once per battle, above a game's army list (0.9.3). */
#app .one-shot { display: grid; gap: 8px; margin: 0 0 16px; }
#app .one-shot .label { margin: 0; }
#app .one-shot-row { padding: 12px 0 0; border-top: 1px solid var(--line); display: grid; gap: 4px; }
#app .one-shot-row p { margin: 0; }
#app .one-shot-name b { font-weight: 700; }
#app .one-shot-name span { color: var(--ink-dim); font-size: var(--t-small); }
#app .one-shot-actions { display: flex; gap: 8px; margin-top: 6px; }
#app .one-shot-actions button { min-height: 48px; flex: 1 1 0; }
#app ul.game-strats > li.is-used .strat-banner { filter: grayscale(1); }
#app ul.game-strats > li.is-used .strat-banner .strat-name { text-decoration: line-through; }
/* The Combat Patrol layout picture is the image itself (no figure around it), and at its
   own width it widened the page past the phone's, which put the floating step bar off
   screen (found 2026-09-26, when the bar arrived). */
#app img.deployment-map { width: 100%; max-width: 100%; height: auto; box-sizing: border-box; }

/* --- 0.7.0: the Collection, the record, Back, the phase foot, Create a game ------------- */
/* Everything here is drawn from the mock-ups Ben answered, M16 to M32 (COLLECTION_BRIEF.md
   section 14), and logged in DESIGN.md 26.13. */

/* Back as an arrow beside the heading (M20: "beside the heading and scrolling with it").
   Accent ink, no fill: a way out, not an action. A box has its own X, so none in one. */
#app .head-back {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; margin: -8px 4px -8px -8px; padding: 0;
  border: 0; border-radius: 999px; background: transparent; color: var(--accent);
  vertical-align: -7px; cursor: pointer;
}
#app .head-back svg { width: 22px; height: 22px; }
#app .dialog .head-back, #app .scrim .head-back { display: none; }
#app .phase-screen > .masthead { padding-top: 0; }

/* Lists and Collection in halves across the row (M21). */
#app .forces-switch { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
#app .forces-switch > .chip { width: 100%; justify-content: center; text-align: center; }

/* The Collection as a list (M16 B, M21). */
#app ul.record-rows { list-style: none; margin: 0 0 6px; padding: 0; }
#app .record-row { border-bottom: 1px solid var(--line); }
#app .record-row:last-child { border-bottom: 0; }
#app .record-row.is-faded .record-open { color: var(--ink-dim); }
#app .record-open {
  display: grid; grid-template-columns: 22px 1fr auto 14px; align-items: center; gap: 8px;
  width: 100%; min-height: 48px; padding: 6px 0; border: 0; background: transparent;
  color: var(--ink); text-align: left; cursor: pointer; font: inherit;
}
#app .record-open::after {
  content: ""; width: 8px; height: 8px; border-right: 2px solid var(--ink-dim);
  border-top: 2px solid var(--ink-dim); transform: rotate(45deg); justify-self: center;
}
#app .record-open > .name { font-size: var(--t-lead); font-weight: 600; min-width: 0; }
#app .record-open > .name > .sub { display: block; margin-top: 1px; font-size: var(--t-small); color: var(--ink-dim); font-weight: 400; }
#app .record-open > .n { font-size: var(--t-lead); font-weight: 700; font-variant-numeric: tabular-nums; }
#app .record-open .has-photo { width: 18px; height: 18px; color: var(--ink-dim); display: block; }
/* A unit's banner with its records under it (0.9.4, Ben: "Collapse all plague marines into
   one banner"). The banner is the unit and its models; the records sit under a rule, as an
   attached unit sits under its bodyguard on a list. */
#app .unit-group { border-bottom: 1px solid var(--line); padding-bottom: 4px; }
#app .unit-group:last-child { border-bottom: 0; }
#app .unit-head { display: grid; grid-template-columns: 22px 1fr auto 14px; gap: 8px;
  align-items: center; min-height: 44px; padding: 6px 0 2px; }
#app .unit-head > .name { grid-column: 2; font-size: var(--t-lead); font-weight: 700; }
#app .unit-head > .n { font-size: var(--t-lead); font-weight: 700; font-variant-numeric: tabular-nums; }
#app ul.unit-records { list-style: none; margin: 0 0 0 30px; padding: 0 0 0 10px;
  border-left: 2px solid var(--line); }
#app .unit-records .record-row { border-bottom: 1px solid var(--line); }
#app .unit-records .record-row:last-child { border-bottom: 0; }
#app .unit-records .record-open { grid-template-columns: 22px 1fr auto 14px; }
#app .unit-records .record-open > .name { font-size: var(--t-body); }
#app .unit-records .record-open > .name.is-loadout { font-weight: 600; }
/* Needed for army list(s), with See full wishlist beside it (M27.1, M30.1). */
#app [data-role="wanted"] .group-label { align-items: center; }
#app [data-role="wanted"] .group-label .chip { font-size: var(--t-small); letter-spacing: 0; text-transform: none; }

/* The record (M17 B, M22, M25, M28, M31). */
#app .dialog .record-under { margin: -6px 0 10px; color: var(--ink-dim); font-size: var(--t-body); }
#app .dialog .record-banner {
  position: relative; margin: 0 -14px; aspect-ratio: 2 / 1; overflow: hidden;
  background: var(--inset); touch-action: none;
}
#app .dialog .record-banner > img { position: absolute; max-width: none; pointer-events: none; user-select: none; }
#app .dialog .record-banner.is-editing { outline: 2px dashed var(--accent); outline-offset: -2px; }
#app .dialog .record-banner .hint {
  position: absolute; top: 38%; left: 50%; transform: translateX(-50%); padding: 4px 10px;
  border-radius: 999px; background: color-mix(in srgb, var(--ground) 80%, transparent);
  color: var(--ink); font-size: var(--t-small); font-weight: 600; white-space: nowrap; pointer-events: none;
}
#app .dialog .banner-bar { position: absolute; left: 14px; right: 14px; bottom: 10px; display: flex; gap: 8px; align-items: center; }
#app .dialog .banner-bar .grow { flex: 1; }
#app .dialog .banner-bar .chip { background: color-mix(in srgb, var(--raised) 88%, transparent); }
#app .dialog .record-noband {
  display: flex; align-items: center; gap: 10px; margin: 0 0 12px; padding: 12px;
  border: 1px dashed var(--edge); border-radius: var(--radius); color: var(--ink-dim); font-size: var(--t-small);
}
#app .dialog [data-role="frame-uses"] { margin: 8px 0 0; }
#app .dialog .record-loadout { margin: 12px 0 2px; font-size: var(--t-lead); font-weight: 600; }
#app .dialog .record-counts { margin: 8px 0 4px; grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); grid-auto-flow: column; }
#app .dialog .record-counts .figure { display: flex; flex-direction: column; justify-content: space-between; align-items: center; text-align: center; }
/* Each title centred in a two-line band, so the titles and the counts line up (M31). */
#app .dialog .record-counts .figure > .key { min-height: 2.6em; display: flex; align-items: center; justify-content: center; line-height: 1.3; margin: 0; }
#app .dialog .record-counts .figure > b { margin-top: 6px; font-size: var(--t-display); }
/* Room between the boxes, and How many centred (M22, M25). */
#app .dialog .chips { display: flex; flex-wrap: wrap; gap: 8px; }
#app .dialog .stepper-row { justify-content: center; gap: 18px; align-items: center; }
#app .dialog .stepper-row .step-number { min-width: 2.2em; text-align: center; font-size: var(--t-lead); }
#app .dialog .stepper-row .chip { min-width: 44px; justify-content: center; }
#app .dialog .shelf-section > .field { display: block; }
#app .dialog .shelf-section > .field > select { width: 100%; }
#app ul.history > li { grid-template-columns: 7.5em 1fr auto; align-items: center; }
#app ul.history .chip { padding: 0 10px; }
#app .dialog [data-role="remove-record"] { margin-top: 14px; }
#app .dialog .add-nickname { display: block; margin: 12px 0 4px; }

/* The phase foot (M26.2): each player's stratagems, each player's army, then Undo, Redo
   and Next. Under a pop-up's shade (50), found drawing M29: it sat over the shade. */
#app .step-nav { z-index: 45; }
#app .step-nav.phase-foot { display: block; }
#app .phase-foot .foot-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 0 0 8px; }
#app .phase-foot .foot-next { display: grid; grid-template-columns: auto auto 1fr; gap: 8px; }
#app .phase-foot button { width: 100%; min-height: 44px; padding: 0 8px; font-size: var(--t-body); line-height: 1.15; }
#app .phase-foot button > b { color: var(--ink-dim); font-weight: 600; margin-left: 4px; }
#app .phase-foot button:disabled { border-style: dashed; color: var(--ink-faint); }
#app .phase-foot .primary { min-height: 48px; font-size: var(--t-lead); }
#app .phase-screen { padding-bottom: 300px; }
body:has(.phase-screen) { --back-lift: 0px; }
/* The current step alone (M26, M29.1): chosen, as its row was. */
#app .phase-steps .step-now {
  display: flex; align-items: center; gap: 8px; min-height: 36px; margin: 4px 0 8px; padding: 0 10px;
  border: 2px solid var(--accent); border-radius: var(--radius-small); color: var(--ink);
  font-size: var(--t-body); font-weight: 600;
}
#app .phase-steps .step-now > .name { flex: 1; }
#app .dialog .use-this { margin: 14px 0 4px; }

/* Create a game (M32): the choices as tiles (F1), chosen by E1, the games with a line under
   the name and set to the left as M32.1 drew them. */
#app .create .choices { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 6px 0 4px; }
#app .create .choices.formats { grid-template-columns: 1fr; }
#app .create .choices .tile-name { min-height: 48px; }
#app .create .choices.formats .tile-name { align-items: flex-start; text-align: left; padding: 10px 14px; min-height: 64px; }
#app .create .choices.formats .tile-name .wnotes { margin: 0; font-weight: 400; }
#app .create .choices .tile.on { border-width: 2px; }
#app .create .choices .tile-name[disabled] { cursor: default; }
#app .create { padding-bottom: 140px; }

/* Your data, in Settings (M19). */
#app .your-data > .primary, #app .your-data > .secondary { width: 100%; margin: 0 0 8px; }

/* --- 0.7.1: the Standard setup, Ben's pass of 2026-09-27 ------------------------------- */
/* The list as wide as the name (DESIGN.md 26.19). */
#app .play .sides .field select { width: 100%; min-height: var(--tap); }
/* Room after a result, before what follows it: "spacing between things like cards and
   rolls and subsequent text looks a bit too close". */
#app .play .step .revealed + .label,
#app .play .step .revealed + p,
#app .play .step .layouts-inline + .label,
#app .play .step .rolloff + .again,
#app .play .step .chips + .label { margin-top: 16px; }
#app .play .step .label.sub { margin-top: 14px; }
#app .play .step .dealt { margin-bottom: 10px; }
#app .play .step .deployment-map { margin-bottom: 12px; }
#app .play .step .message + .reveal-button,
#app .play .step .message + .label,
#app .play .step .step-fold + .label,
#app .play .step .message + div > .label:first-child,
#app .play .step .message + div > .reveal-button:first-child { margin-top: 14px; }
#app .play .step > div, #app .play .step > .handover { margin-top: 12px; }
#app .play .blocked { margin-top: 14px; }
/* Roll again and Draw again: the quiet button under the result. */
#app .play .again { margin: 8px 0 4px 30px; }
#app .play .revealed .again, #app .play .step-fold .again { margin-left: 0; }
#app .play .again-button { min-height: 40px; padding: 0 16px; }
/* Designer's Notes and Create your own battlefield: folds where they stand. */
#app .play .step-fold { margin: 12px 0 0 30px; }
#app .play .step-fold > .message { margin-left: 0; }
#app .play .step-fold[open] { padding-bottom: 8px; }
#app .play .step-fold .dealt, #app .play .step-fold .reveal-button { margin-left: 0; }
/* Each player's own roll (26.20): a tile each, player one on the left. */
#app .play .rolloff { margin: 12px 0 0 30px; }
#app .play .rolloff .roll-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
#app .play .roll-tile {
  display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 12px 10px;
  border: 1px solid var(--line); border-radius: var(--radius-small); min-height: 104px;
}
#app .play .roll-tile .label { margin: 0; }
#app .play .roll-tile[data-won="true"] { border: 2px solid var(--accent); }
#app .play .roll-tile > button { width: 100%; min-height: 48px; }
#app .play .rolloff .tied { margin: 0 0 8px; }
#app .play .rolloff .won { margin: 10px 0 0; }
#app .play .chooser { margin: 14px 0 6px 30px; }
#app .play .step .chooser + .chips { margin-left: 30px; }
/* Step 2's reveal as two tiles (26.21), player one on the left. */
#app .play .reveal-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 12px 0 0 30px; }
#app .play .reveal-tile {
  display: flex; flex-direction: column; padding: 12px 10px;
  border: 1px solid var(--line); border-radius: var(--radius-small);
}
#app .play .reveal-tile .who { margin: 0 0 10px; font-size: var(--t-lead); font-weight: 700; color: var(--ink); }
#app .play .reveal-tile .label { margin: 0 0 2px; }
#app .play .reveal-tile .value { margin: 0 0 10px; font-weight: 700; color: var(--ink); }
#app .play .reveal-tile .mission-link {
  min-height: var(--tap); padding: 6px 8px; text-align: left; font-weight: 700;
  font-size: var(--t-body); color: var(--accent); background: transparent; border: 1px solid var(--line);
  border-radius: var(--radius-small); line-height: 1.25;
}
#app .play .handover .unfinished { margin: 8px 0 0; }
#app .play .step { scroll-margin-top: 8px; }
#app .play .step .step-sub { margin: 14px 0 0 30px; }
#app .play .step .step-list { margin: 6px 0 0 30px; padding-left: 18px; }
#app .play .step .step-list li { margin: 0 0 6px; }
#app .play .step .step-list li::marker { color: var(--ink-faint); }
#app .play .step-fold .step-sub, #app .play .step-fold .step-list { margin-left: 0; }
#app .play .step .step-fold[data-role="custom-battlefield"] { margin-left: 0; }
#app .play .roll-tile .rolling { margin: 4px 0; }
#app .play .step .step-list { text-indent: 0; }
/* A tile's name clear of its corner i (found drawing M44.1: "ENGAGE ON ALL FRONTS" ran
   under it). Both sides, so the name stays centred. */
#app .tile:has(> .qualifier) > .tile-name { padding-left: 22px; padding-right: 22px; }

/* --- 0.7.3: the game screen phase by phase, after Ben's first game with friends -------- */
/* The Missions button at the top right of the phase's heading (26.23). */
#app .phase-head .head-row { display: flex; align-items: flex-start; gap: 10px; }
#app .phase-head .head-row > h1 { flex: 1 1 auto; min-width: 0; }
#app .phase-head .head-missions { flex: 0 0 auto; margin-top: 4px; }
/* Every step a closed banner (26.24). */
#app .phase-steps { margin: 12px 0 0; }
#app .phase-steps .phase-step { margin-top: 0; }
#app .phase-steps .phase-step > .message { margin: 0 0 10px; }
/* The phase's own roll (26.25). */
#app .quick-rolls { margin: 16px 0 0; }
#app .quick-roll + .quick-roll { margin-top: 12px; }
#app .quick-roll .dealt { margin: 10px 0 0; gap: 10px; }
#app .quick-roll .total { color: var(--ink-dim); font-size: var(--t-small); }
#app .quick-roll .total b { color: var(--ink); font-size: var(--t-lead); margin-left: 4px; }
/* Each player's buttons in their faction's colour (26.26). */
#app .phase-foot button[style*="--faction-colour"] {
  background: color-mix(in srgb, var(--faction-colour) 30%, var(--raised));
  border-color: color-mix(in srgb, var(--faction-colour) 75%, var(--line));
}
#app .missions-of { margin: 0 0 18px; padding: 0 0 0 10px; border-left: 4px solid var(--line); }
#app .missions-of[style*="--faction-colour"] { border-left-color: var(--faction-colour); }
#app .missions-of .missions-name { margin: 0 0 6px; font-size: var(--t-lead); }
#app .card .vp-read { font-size: var(--t-small); color: var(--ink); white-space: nowrap; margin-left: 8px; }
#app .nothing-scores { margin: 16px 0 0; }
/* Selecting Secondary Missions (26.27, M44 A). */
#app .mode-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 0 0 12px; }
#app .mode-choice {
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px; min-height: 96px;
  padding: 12px; text-align: left; border: 1px solid var(--line); border-radius: var(--radius-small);
  background: var(--raised); color: var(--ink); font-size: var(--t-body);
}
#app .mode-choice.on { border: 2px solid var(--accent); }
#app .mode-choice b { font-size: var(--t-lead); }
#app .mode-choice span { font-size: var(--t-small); color: var(--ink-dim); line-height: 1.35; }
#app .fixed-head { display: flex; justify-content: space-between; align-items: baseline; margin: 12px 0 6px; }
#app .fixed-head b { color: var(--accent); }
#app .fixed-row { border: 1px solid var(--line); border-radius: var(--radius-small); padding: 10px 12px;
  margin: 0 0 8px; background: var(--raised); }
#app .fixed-row.on { border: 2px solid var(--accent); }
#app .fixed-top { display: flex; align-items: center; gap: 8px; }
#app .fixed-top .fixed-name { flex: 1 1 auto; min-width: 0; }
#app .fixed-top .qualifier { margin: 0; }
#app .fixed-when { margin: 4px 0 0; font-size: var(--t-small); color: var(--ink-dim); }
#app .fixed-line { margin: 4px 0 0; font-size: var(--t-small); color: var(--ink); }
#app .fixed-line b { color: var(--accent); }
#app .play .reveal-tile .note-small { margin: 0 0 8px; font-size: var(--t-small); color: var(--ink-dim); }
#app .play .reveal-tile .mission-link + .mission-link { margin-top: 6px; }
/* Declare Battle Formations as three tabs (26.28, M45 C). */
#app .formations .meter { margin: 0 0 12px; }
#app .formations .meter-nums { display: flex; justify-content: space-between; margin: 0;
  font-size: var(--t-small); color: var(--ink-dim); }
#app .formations .meter-nums b { color: var(--ink); }
#app .formations .meter-bar { height: 8px; border-radius: var(--radius-small); background: var(--inset); overflow: hidden; margin: 6px 0 0; }
#app .formations .meter-bar > i { display: block; height: 100%; background: var(--accent); }
#app .formation-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin: 0 0 10px; }
#app .formation-tabs .chip { justify-content: center; }
#app .formations .formation { border-bottom: 1px solid var(--line); padding: 6px 0; }
#app .formations .formation-row { display: flex; align-items: center; gap: 10px; min-height: 48px; }
#app .formations .formation-name { flex: 1 1 auto; min-width: 0; margin: 0; }
#app .formations .formation-name > b { display: block; font-weight: 600; }
#app .formations .formation-name > span { display: block; font-size: var(--t-small); color: var(--ink-dim); }
#app .dialog .move-to > button { width: 100%; min-height: 48px; margin: 0 0 8px; }
#app .dialog .move-to > .wnotes { margin: -2px 0 10px; }
#app .formation-tabs .chip { min-width: 0; padding: 0 6px; font-size: var(--t-small); white-space: nowrap; }
#app .end-turn-scoring { margin: 4px 0 12px; }

/* --- 0.7.4: the Dataslate, Ben's picks of M33 to M43 (DESIGN.md 26.29 to 26.35) -------- */

/* The Dataslate's search, and more room under it before Core Rules (M33.2: "Slightly
   spacing increase below the search bar to core rules"). */
#app .dataslate-search { margin: 0 0 20px; }
#app .dataslate-search > .search { margin-bottom: 0; }
/* A heading over a group of rows, its count on the right. */
#app .group-label.list-group { margin: 22px 0 8px; }
#app .home > .group-label.list-group:first-of-type,
#app .result-group:first-child > .group-label.list-group,
#app .alliance:first-of-type > .group-label.list-group,
#app .rule-group:first-of-type > .group-label.list-group,
#app .menu-group:first-of-type > .group-label.list-group { margin-top: 12px; }
/* A faction's band as a link (H2): white on its own colour, count right, chevron. */
#app a.band, #app .band {
  display: flex; align-items: center; gap: 12px; min-height: 48px; margin: 0 0 6px;
  padding: 0 12px; border-radius: var(--radius-small);
  background: var(--faction-colour, var(--raised)); color: var(--on-strat);
  font-size: var(--t-lead); font-weight: 700; text-decoration: none;
}
#app .band:not([style]) { color: var(--ink); }
#app .band > .title { flex: 1; min-width: 0; }
#app .band > .count { font-size: var(--t-small); font-weight: 600; white-space: nowrap; }
#app a.band::after {
  content: ""; width: 8px; height: 8px; border-right: 2px solid currentColor;
  border-top: 2px solid currentColor; transform: rotate(45deg);
}
/* The band under a faction's name, which is not a link (M39.2). */
#app .band.band-head { margin: 0 0 14px; }
/* A search hit: its name, and where it is under it. */
#app .search-results .line { align-items: baseline; }
#app .search-results .name > .sub, #app .lines .name > .sub {
  display: block; margin-top: 1px; color: var(--ink-dim); font-size: var(--t-small); font-weight: 400;
}
#app .see-all { margin: 6px 0 0; }
#app .found { margin: 4px 0 12px; }
#app .slim-band { margin: 16px 0 4px; }
#app .result-group:first-of-type > .slim-band { margin-top: 4px; }
/* A section screen's kinds (M35.3) and a faction's role folds (M39.2). */
#app .kind-group + .kind-group { margin-top: 4px; }
#app .role-fold .lines { margin-top: 4px; }
/* Combat Patrols by faction as folds in their colours (M43.2). */
#app .patrol-fold > .abilities { margin: 10px 0 0; }
#app .patrol-fold > .abilities > .group-label.list-group { margin-top: 12px; }
/* The card key's rows, which say rather than open (M41.2). */
#app .key-row > .name { font-weight: 600; }
#app .key-group + .key-group > .group-label.list-group { margin-top: 22px; }
#app .patrol-fold { margin: 0 0 8px; border-bottom: 0; }
#app .rules > .section-rows { margin-top: 12px; }
/* The End turn prompt's two buttons stay in view while its scoring scrolls (26.29). */
#app .dialog .end-turn-actions { position: sticky; bottom: -14px; margin: 0 0 -14px;
  padding: 12px 0 14px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
  background: var(--raised); box-shadow: 0 -8px 12px -8px var(--ground); }
#app .dialog .end-turn-actions > button { width: 100%; margin: 0; }
/* A Core Rules section's own screen draws its rows as the index's folds did (26.31). */
#app .rule-section .lines .name { font-size: var(--t-lead); }
#app .rule-section .lines > .kids { list-style: none; margin: 0 0 4px 3px; padding: 0 0 0 11px;
  border-left: 1px solid var(--line); }
#app .rule-section .lines > .kids > .lines { padding-bottom: 0; }
#app .rule-section .line.child { border-bottom: 0; padding: 2px 0; }
#app .rule-section .line.child .name { font-weight: 400; color: var(--ink-dim); }
#app .rule-section .lines .points { font-size: var(--t-micro); font-weight: 400; color: var(--ink-faint); }

/* --- 0.7.5: the roller -------------------------------------------------------------- */
/* [LETHAL HITS]' automatic wounds, on the right of the wound dice behind a rule (26.36). */
#app .roll .dice .auto-dice { display: inline-flex; flex-wrap: wrap; gap: inherit;
  margin-left: auto; padding-left: 10px; border-left: 2px solid var(--line); }
#app .roll [data-role="kind"] { margin: 8px 0 0; }

/* A side of the attack as one card, 0.7.5 (DESIGN.md 26.37, M47 A and M47.6). */
#app .roll .side-card .choose { margin: 0; text-align: center; font-weight: 700;
  background: var(--fill); border-color: var(--fill); color: var(--onfill); min-height: 48px; }
#app .roll .side-card .choose-note { margin: 8px 0 0; }
#app .roll .side-card .edit-row { margin: 10px 0 0; }
#app .roll .side-card .side-head { display: flex; align-items: flex-start; gap: 10px; margin: 0 0 10px; }
#app .roll .side-card .side-head .pick.unit { flex: 1 1 auto; min-width: 0; min-height: 48px; margin: 0;
  padding: 6px 12px; display: flex; flex-direction: column; justify-content: center; }
#app .roll .side-card .side-head .pick.unit b { font-size: var(--t-lead); font-weight: 700;
  overflow-wrap: break-word; }
#app .roll .side-card .side-head .pick.unit span { font-size: var(--t-small); color: var(--ink-dim); }
#app .roll .side-card .count { flex: 0 0 auto; text-align: center; }
#app .roll .side-card .stepper { display: flex; align-items: center; height: 48px;
  border: 1px solid var(--line); border-radius: var(--radius-small); background: var(--inset); }
#app .roll .side-card .stepper button { width: 40px; height: 46px; border: 0; background: transparent;
  color: var(--accent); font-size: var(--t-lead); font-weight: 700; }
#app .roll .picker .pick-steps .chip[disabled] { border-style: dashed; }
#app .roll .side-card .stepper input { width: 34px; min-height: 0; height: 46px; padding: 0; border: 0;
  background: transparent; color: var(--ink); text-align: center; font-size: var(--t-lead);
  font-weight: 700; font-variant-numeric: tabular-nums; }
#app .roll .side-card .count-label { margin: 4px 0 0; font-size: var(--t-micro); font-weight: 700;
  letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--ink-faint); }
#app .roll .side-card .uses { display: flex; align-items: center; gap: 10px; min-height: 48px;
  margin: 0 0 10px; padding: 6px 12px; border: 1px solid var(--line); border-radius: var(--radius-small); }
#app .roll .side-card .uses > div { flex: 1 1 auto; min-width: 0; }
#app .roll .side-card .uses b { display: block; font-weight: 600; }
#app .roll .side-card .uses span { display: block; font-size: var(--t-small); color: var(--ink-dim); }
#app .roll .side-card .read-stats { margin: 0; }
/* The picker's two steps, and a weapon as one row with its profile (M47.4). */
#app .roll .picker .pick-steps { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 0 0 12px; }
#app .roll .picker .pick-steps .chip { justify-content: center; }
#app .roll .picker .weapon-rows > .label { margin: 12px 0 4px; }
#app .roll .picker .weapon-row { display: flex; align-items: center; gap: 10px; min-height: 52px;
  padding: 6px 0; border-bottom: 1px solid var(--line); cursor: pointer; }
#app .roll .picker .weapon-row .what { flex: 1 1 auto; min-width: 0; }
#app .roll .picker .weapon-row .what b { display: block; font-weight: 600; }
#app .roll .picker .weapon-row .what span { display: block; font-size: var(--t-small); color: var(--ink-dim); }
#app .roll .picker .weapon-row.on .what b { color: var(--accent); }
/* The sequence, 0.7.6 (DESIGN.md 26.39, M46 B): the counts held at the top, earlier steps
   as one line each, the next roll asked in a card. */
#app .roll .tally-bar { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 5;
  /* Each figure as wide as its name and the space shared out between them: equal thirds
     broke UNSAVED and IGNORED over two lines at phone width. */
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 6px;
  /* The page's own gutter, not 16px: at 16 against a 14px gutter the bar was 2px wider
     than the screen and the whole roller scrolled sideways (sweep of 2026-09-27). */
  margin: 0 calc(-1 * var(--gap)) 6px; padding: 10px var(--gap) 8px; background: var(--ground);
  border-bottom: 1px solid var(--line); }
#app .roll .tally-bar > div { flex: 0 0 auto; text-align: center; }
#app .roll .tally-bar b { display: block; height: 1.4em; line-height: 1.4em; font-size: var(--t-lead); font-weight: 700;
  font-variant-numeric: tabular-nums; color: var(--ink); }
#app .roll .tally-bar .now b { color: var(--accent); }
#app .roll .tally-bar span { display: block; font-size: var(--t-micro); font-weight: 700;
  letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--ink-faint);
  white-space: nowrap; }
#app .roll .step.is-folded { padding: 0; }
#app .roll .step.is-folded .fold-head { display: flex; align-items: center; gap: 10px; width: 100%;
  min-height: 48px; padding: 0; border: 0; background: transparent; color: var(--ink); font: inherit;
  text-align: left; cursor: pointer; }
#app .roll .step.is-folded .fold-head .title { flex: 1 1 auto; min-width: 0; font-size: var(--t-body);
  font-weight: 600; }
#app .roll .step.is-folded .fold-head .result { font-size: var(--t-small); color: var(--ink-dim); }
#app .roll .step.is-folded .fold-head .result b { color: var(--ink); font-size: var(--t-body); }
#app .roll .step.is-folded .fold-head::after { content: ""; flex: 0 0 auto; width: 7px; height: 7px;
  border-right: 2px solid var(--ink-faint); border-bottom: 2px solid var(--ink-faint);
  transform: rotate(45deg); margin: 0 4px 4px 0; }
#app .roll .step.is-folded.is-open .fold-head::after { transform: rotate(-135deg); margin: 4px 4px 0 0; }
#app .roll .step.is-folded .fold-body { padding: 0 0 10px; }
#app .roll .step.is-folded.is-open .dice { display: flex; margin-top: 0; }
#app .roll .ready-card { margin: 12px 0 0; padding: 14px; border: 2px solid var(--accent);
  border-radius: var(--radius-small); background: var(--raised); }
#app .roll .ready-card header { display: flex; align-items: center; gap: 10px; }
#app .roll .ready-card header h3 { flex: 1 1 auto; margin: 0; font-size: var(--t-lead); }
#app .roll .ready-card .num { flex: 0 0 auto; width: 20px; height: 20px; border: 1px solid var(--line);
  border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--t-micro); color: var(--ink-dim); }
#app .roll .ready-card .need { margin: 12px 0 2px; font-size: var(--t-display); font-weight: 700;
  line-height: 1.15; color: var(--accent); font-variant-numeric: tabular-nums; }
#app .roll .ready-card .need small { font-size: var(--t-lead); color: var(--ink); }
#app .roll .ready-card .because { margin: 2px 0 0; font-size: var(--t-small); color: var(--ink-dim); }
#app .roll .ready-card .aside { margin: 4px 0 0; font-size: var(--t-small); color: var(--ink-dim); }
#app .roll .ready-card .count { margin: 10px 0 12px; font-size: var(--t-body); color: var(--ink); }
#app .roll .ready-card > .primary { width: 100%; min-height: 48px; }

/* =========================================================================================
   0.8.1: THE DESIGN REVIEW, SYSTEM-WIDE (DESIGN.md section 27).

   Ben picked each of these on 2026-09-28. They sit at the end of the file so each is the
   rule that holds over the older one it corrects, and each names its DR. What is only a
   word or a class, not a look, was changed where it is written (views.py and the scripts).
   ========================================================================================= */

/* --- DR2: 17px for a tile's or band's name, 20px for a big figure (TYPOGRAPHY.md). ------ */
#app :is(.play .reveal-tile .who, .mode-choice b, .missions-of .missions-name,
         a.band, .band) {
  font-size: var(--t-name-large);
}
#app :is(.dialog .record-counts .figure > b, .roll .ready-card .need, .roll .step .headline b,
         .play .drawn-letter, .game .tally b) {
  font-size: var(--t-figure);
}

/* --- DR1: the tally's latest figure in accent, as 26.39 says (K1's named exception). ---- */
#app .roll .tally-bar .now b { color: var(--ink); }
/* `.latest` twice to outrank K1's ink list above, whose `:is()` takes the specificity of
   its heaviest member (four classes); that is what kept this ink since 0.7.6. */
#app .roll .tally-bar .latest.latest b { color: var(--accent); }

/* --- DR14: a result the app drew or rolled for you is ink (K1). ------------------------- */
#app .play :is(.derived b, .dealt b) { color: var(--ink); }

/* --- DR5: every fold's header is R2: 15px 700, the words' own case, 48px. --------------- */
#app .aside > summary {
  min-height: 48px;
  font-size: var(--t-lead);
  font-weight: 700;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
}
#app .aside > summary .badge { letter-spacing: 0; text-transform: none; }

/* --- DR6: a heading in the publisher's text is a T3 heading in its own case. ------------ */
#app .play .step .step-sub {
  margin-top: 14px;
  color: var(--ink);
  font-size: var(--t-lead);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
}

/* The same pick on a rule's own headings ("Moving a Model in a Straight Line"), which
   were 11px capitals too: the review found DR6 "spreading beyond setup". */
#app .verbatim h3.printed,
#app .printed-heading {
  color: var(--ink);
  font-size: var(--t-lead);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
}

/* --- DR15: all rules text is T4: 13px, full ink, first line indented. ------------------- */
#app .verbatim p.printed:not(.step) { font-size: var(--t-body); text-indent: 1.25em; color: var(--ink); }
#app .verbatim ul.lines-of-text li,
#app ul.message.step-list li { font-size: var(--t-body); }
#app .rule-pop-body .message { font-size: var(--t-body); }

/* --- DR16: the app's own sentences are T9: 12px 400 dim, never indented. ---------------- */
#app .message.app { color: var(--ink-dim); font-size: var(--t-small); text-indent: 0; }

/* --- DR17: the context line under a title is small and dim, then a hairline (N1). ------- */
#app .masthead { padding-bottom: 10px; border-bottom: 1px solid var(--line); }
#app .masthead[data-sticky="true"] { padding-bottom: 8px; }
#app .masthead > p.detachment,
#app .masthead > p.detachment a { color: var(--ink-dim); font-size: var(--t-small); font-weight: 400; }
#app .masthead > p.detachment { margin: 0; }

/* --- DR18: in light, a disabled button's dash takes --edge, since --line on the inset
   ground all but vanished (B3). ---------------------------------------------------------- */
:root[data-theme="light"] #app button:disabled,
:root[data-theme="light"] #app button:disabled:is(.primary, .secondary, .chip, .link) {
  border-color: var(--edge);
}

/* --- DR19: a datasheet's column headings and its labels are T6, its keywords in the
   publisher's capitals (served that way, not set with CSS). ------------------------------ */
#app .weapons thead th {
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}
#app :is(.composition, .abilities) .field > .key {
  display: block;
  margin: 10px 0 4px;
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

/* --- DR20: every box opens at one height, its title level with the X (D1, D2). ---------- */
#app .scrim, .scrim { align-items: flex-start; }
.dialog-head { align-items: center; padding: 8px 6px 8px 16px; min-height: 52px; }
.dialog-head[data-titled="false"] { min-height: 0; }
#app .dialog-head > h2 { flex: 1 1 auto; min-width: 0; }
/* The chip names the rule the box holds and opens nothing, so it is not orange (K1). */
#app .dialog-head > .clause { flex: none; border-color: var(--line); color: var(--ink-dim); }

/* --- DR21: a section's count on its row is 12px dim (T12); points stay 15px 700 ink. ---- */
#app .section-rows .line .points,
#app .lines .line.section-row .points {
  color: var(--ink-dim);
  font-size: var(--t-small);
  font-weight: 600;
}

/* --- DR22: one chevron, one size, pinned to the row's right edge (L6). ------------------ */
#app :is(.rule-open, .record-open, .detachment-row.wide > a.name,
         .detachment-row.wide > .name.explains)::after,
#app .detachment-row:not(.wide):has(> a.name, > .name.explains)::after,
#app a.band::after {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  margin: 0 4px 0 auto;
  border: 0;
  border-right: 2px solid var(--ink-faint);
  border-bottom: 2px solid var(--ink-faint);
  transform: rotate(-45deg);
}
#app a.band::after { border-color: currentColor; }

/* --- DR12: a dense list row is a whole-row target at least 44px tall, with the chevron
   (R1, amended 0.8.1: "44px rows"). --------------------------------------------------------- */
#app .lines > .line:has(> a.name):not(:has(button, a:not(.name), input, select)) {
  position: relative;
  align-items: center;
  min-height: 44px;
  padding-right: 22px;
}
#app .lines > .line:has(> a.name):not(:has(button, a:not(.name), input, select)) > a.name::before {
  content: ""; position: absolute; inset: 0;
}
#app .lines > .line:has(> a.name):not(:has(button, a:not(.name), input, select))::after {
  content: "";
  position: absolute;
  right: 6px;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-top: -4px;
  border-right: 2px solid var(--ink-faint);
  border-bottom: 2px solid var(--ink-faint);
  transform: rotate(-45deg);
  pointer-events: none;
}

/* --- DR13: every inline link is underlined, wherever it sits (L1). ---------------------- */
#app :is(button.explains, a.explains, .name.explains):not(.chip):not(.name) ,
#app .list-summary .field button.name.explains,
#app .masthead button.name.explains,
#app .led-by a.explains {
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent) 50%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* --- DR24: a setup step has one left edge, where its text starts under its number. ------ */
#app .play .step > :not(header) { margin-left: 30px; }
#app .play .step > :not(header) :is(.sides, .dealt, .again, .rolloff, .handover,
                                    .message, .note, .blocked, .step-sub, .reveal-tiles,
                                    .layouts-inline) {
  margin-left: 0;
}

/* --- DR27: red only for the opponent, an error, or a destroy in a confirm (K3). --------- */
#app .play .blocked,
#app .play .blocked .label { color: var(--ink-dim); }

/* --- DR8: Attack in sentence case carries no tracking (N3). ----------------------------- */
#app .roll [data-role="attack"] { letter-spacing: 0; text-transform: none; }

/* --- DR27, the roster: the red x is a B5 chip reading Remove (36px, 10px corners, as
   every chip). Only the old x's 22px box is undone here. ---------------------------------- */
#app .build-row > .chip.kill { width: auto; display: inline-flex; }
#app .build-row > .chip.kill::after { inset: -4px -2px; }

/* --- DR19: the weapon table's column headings keep T6's capitals without its tracking,
   which pushed RANGE into A in a column 5.5ch wide. ------------------------------------- */
#app .weapons thead th { letter-spacing: 0; }

/* =========================================================================================
   0.8.2: THE DESIGN REVIEW, SCREEN BY SCREEN (DESIGN.md section 27, "By area").
   ========================================================================================= */

/* --- Dataslate --------------------------------------------------------------------------- */
/* Home's Built on: the app's small dim words, one line each (T9). */
#app .sources .source-line { margin: 2px 0 0; }
/* FAQ questions at 15px 400: a sentence, not a name (Ben: "questions at 15px 400"). */
#app [data-role="faq"] .detachment-row .name.explains { font-weight: 400; }
/* A detachment box's context line (N1) and an enhancement's cost at 15px 700 ink (T12). */
#app .box-context { margin: 0 0 10px; color: var(--ink-dim); font-size: var(--t-small); }
#app [data-role="enhancements"] .who .cost {
  margin-left: 8px;
  color: var(--ink);
  font-size: var(--t-lead);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* A stratagem box's strip as its list row: the kind on one line at the left, the CP right,
   and one even gap between the fields (Ben: "match the list"). */
#app .strat-head { justify-content: space-between; align-items: center; }
#app .strat-head .strat-kind { letter-spacing: 0; white-space: nowrap; }
#app .strat-body .field { margin: 0 0 10px; }
#app .strat-body .field[data-strat-part="effect"] .message { margin-top: 0; }
#app .strat-body .field[data-strat-part="effect"] .message + .message { margin-top: 8px; }
/* The provenance line, clear of what follows it. */
#app .gameplay > [data-role="provenance"] { margin-bottom: 14px; }
/* The units index's empty state as a V1 note: a dim edge and a dim label. */
#app .banner.is-note .banner-label { color: var(--ink-dim); }

/* --- Combat Patrols, deck, layouts, house rules ------------------------------------------ */
/* Add to Forces as a 48px secondary at full width (Ben: "48px Add to Forces"), and a
   row's count kept on one line ("counts on one line"). */
#app .patrol-page .masthead .favourite { width: 100%; }
#app .patrol-page .favourite.on { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
#app .patrol-page .line .points,
#app .patrol-page .detachment-row > .wnotes { white-space: nowrap; flex: none; }
/* Provenance at the foot of the screen (N4). */
#app .foot-provenance { margin: 24px 0 0; }
/* The deck's tiles keep their words clear of the outline ("pad the tile"). */
#app .deck .tile-name { padding-left: 8px; padding-right: 8px; }
/* The layouts card stacked: each label over its value ("stack the card"). */
#app .layouts .setting .row { display: block; }
#app .layouts .setting .row > .label { display: block; margin: 0 0 2px; }
#app .layouts .setting .row + .row { margin-top: 10px; }
/* House rules: a game shape inside a capitals label as written; 8px between a label and
   its chips; the badge a little clear of the rule's name. */
#app .as-written { text-transform: none; letter-spacing: 0; }
#app [data-role="house-modifies"] { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
#app [data-role="house-modifies"] > .label { margin: 0; }
#app [data-role="house-rule"] > .who > .badge { margin-right: 6px; vertical-align: 2px; }

/* --- The roller -------------------------------------------------------------------------- */
/* The two Choose buttons drawn as the secondaries 0.8.1 made them (DR8). A rule from 0.7.5
   still filled them, so on screen they stayed primaries; found drawing 0.8.2. */
#app .roll .side-card .choose.secondary {
  background: transparent;
  border-color: var(--edge);
  color: var(--ink);
}
/* Both toggle sets 8px apart, the unchosen dim (E1; Ben: "8px gap, E1 dim unchosen on
   both sets"). */
#app .roll :is(.switches, [data-role="kind"]) { display: flex; flex-wrap: wrap; gap: 8px; }
#app .roll :is(.switches, [data-role="kind"]) .chip:not(.on) { color: var(--ink-dim); }
/* The ready card holds, so its corners are 14px (section 1). */
#app .roll .ready-card { border-radius: var(--radius); }
/* The chosen side's unit button and its stepper level at the top: the stepper carried the
   loadout stepper's 10px top margin ("align stepper and unit button"). */
#app .roll .side-card .side-head { align-items: flex-start; }
#app .roll .side-card .side-head .stepper { margin: 0; }

/* --- Forces, Builder, Collection, Settings --------------------------------------------- */
/* Z2: the empty line in the app's words, then the way forward 14px under it. */
#app .wnotes.z2 { margin: 8px 0 0; }
#app .wnotes.z2 + .primary, #app .z2-go { margin-top: 14px; }
#app .saved-list.patrol-favourite .shelf-tag { display: inline-block; margin: 0 12px 8px; }
/* The list name and its Back on one line, the name wrapping inside its own box ("Back
   beside the title everywhere"). */
#app .masthead h1.list-name { display: flex; align-items: center; }
#app .masthead h1.list-name .name-open { min-width: 0; }
/* Import's primary 14px under its field (D4). */
#app [data-role="import-field"] + .primary { margin-top: 14px; }
/* A prompt's two buttons in equal halves (P3), wherever the pair sits. */
#app .dialog .actions:has(> button + button:last-child) {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
/* The picker: a taken unit's price in ink, and the V2 badge saying how many are in the
   army (Ben: "taken unit's price stays ink, the in-army badge carries it"). Colour alone
   was the only signal (K1, O2). */
#app .candidate .points.spent { color: var(--ink); }
#app .candidate .in-army.badge {
  border: 0; border-radius: 5px; background: var(--inset); color: var(--ink-dim);
  padding: 2px 6px; min-height: 0; white-space: nowrap;
}
/* The Warlord mark on a roster row as a B5 chip (36px, 10px corners, ink, --edge), on its
   own line under the name. */
#app .build-row > .tag.chip.warlord-chip {
  display: inline-flex; align-items: center; min-height: 36px; margin-top: 6px; padding: 0 12px;
  border: 1px solid var(--edge); border-radius: var(--radius-small);
  color: var(--ink); font-size: var(--t-body); font-weight: 500; letter-spacing: 0;
  text-transform: none; text-decoration: none;
}
/* The chosen enhancement as E1: a 2px accent border all round, its name in ink (Ben: "E1
   border on the chosen enhancement"). It had a left edge only. */
#app .enhancement-row[data-taken="true"] {
  border: 2px solid var(--accent);
  border-radius: var(--radius-small);
  padding: 8px 10px;
}
#app .enhancement-row[data-taken="true"] > .name { color: var(--ink); }
/* With no italics, a quoted rider in a loadout note is told from the app's count by ink:
   the publisher's words in full ink (T4), the app's in dim (T9). Before 0.8.2 the italic
   did this, and without it the two voices rendered the same. */
#app .loadout .why .bit[data-voice="quoted"] { color: var(--ink); }
/* No italics for the app's own voice (Ben: "no italics"). The publisher's own emphasis
   (`.marked.emph`) and the i glyph keep theirs. */
#app :is(.filtered .message, .sheet-head .filtered, .det-blocked,
         .loadout .why .bit[data-voice="ours"], .loadout .carrying .overdrawn,
         .loadout-withheld, .loadout-none, .loadout-refused, .added-note,
         .attach-permitted .who, .game .warned q) { font-style: normal; }
/* A wide row's name takes the row, so its chevron is at the right edge (L6). */
#app .detachment-row.wide > :is(a.name, .name.explains) { width: 100%; }
/* The Collection's figures band: two lines for every label, so the values sit level
   ("two-line figures band"). */
#app .collection .figures .figure > .key {
  display: block; min-height: 2.7em; line-height: 1.35;
}
/* Notes: 96px, no resize grip (M2, B8). */
#app textarea[data-role="notes"] { min-height: 96px; resize: none; }
/* Take off the wishlist as a B2 at full width. */
#app .actions.single > .secondary { width: 100%; }
/* Settings: its section headings as T6 group labels, and a word value in a figure at 700
   like its neighbours' numbers (Ben: "Settings headings as T6 group labels, values at
   700"). */
#app .settings-screen .section-title {
  margin: 22px 0 8px;
  color: var(--ink-faint);
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}
#app .settings-screen > .section-title:first-of-type,
#app .settings-screen .section-title:first-child { margin-top: 12px; }
#app .figure .value-words { color: var(--ink); font-weight: 700; }
/* 26.14's dashed accent outline on the band while framing, drawn over the photo: as an
   outline it sat under the absolutely placed image and did not show ("26.14 outline
   back"). */
#app .dialog .record-banner.is-editing::after {
  content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  border: 2px dashed var(--accent);
}
/* Configure as a B5 chip like Remove: it was a 26px control of its own beside a 36px chip. */
#app .build-row > .edit.chip {
  min-height: 36px; padding: 0 12px; border-radius: var(--radius-small);
  border-color: var(--edge); color: var(--ink); font-size: var(--t-body);
}
#app .build-row > .edit.chip::after { inset: -4px -2px; }

/* --- Create a game and setup ------------------------------------------------------------ */
/* Once one is chosen the others are dim (E1; Ben: "dim unchosen, per E1"). */
#app .create .choices:has(.tile.on) .tile:not(.on) .tile-name { color: var(--ink-dim); }
/* Choose again, Draw again and Roll again at full width, alone under what they act on
   (section 5; Ben: "full width"). */
#app .play .again > .again-button { width: 100%; }
/* DR25, two levels in each instance (Ben 2026-09-28: "AGREE WIT YOUR RECOMMENDATION ON
   THE NESTED BOXES. 2 levels in each instance."): the step, then one card per player;
   what is listed inside the card is rows under hairlines, not boxes. Standard step 6 had
   a box inside the player's card; the formation steps a box per unit. Drawn in
   `mock/dr25/`. */
#app .play .handover .side {
  margin: 0; padding: 0; border: 0; border-radius: 0; background: none;
}
#app .play .side .formation {
  margin: 0; padding: 10px 0; border: 0; border-top: 1px solid var(--line);
  border-radius: 0; background: none;
}
#app .play .side .formation:first-of-type { margin-top: 8px; }
/* A done step's row: its result dim, on every done step (Ben: "dim result on every done
   step's row"); it was 700 full ink on some, louder than the step names. */
#app .play .step .derived-right { color: var(--ink-dim); font-size: var(--t-small); font-weight: 400; }
/* The Use button's cost as a 12px count after the verb. */
#app .use-this > .count { margin-left: 8px; font-size: var(--t-small); font-weight: 600; }
/* The i box's numbered steps: no indent on any, the publisher's own numbers kept. */
#app ol.phase-steps-list { list-style: none; margin: 8px 0 0; padding: 0; }
#app ol.phase-steps-list > li { margin: 2px 0 0; }
/* The Homebrew game: 8px between the CP correction's parts and between a side's two
   buttons (Ben: "8px gaps"). */
#app .cp-adjust .chips { display: flex; align-items: center; gap: 8px; }
#app .side-row .row { display: flex; flex-wrap: wrap; gap: 8px; }

/* --- Game screens --------------------------------------------------------------------------- */
/* One mission card everywhere (Ben: "full-ink headings, VP 15px 700, chip numbers 700,
   FIXED and TACTICAL as V2 badges, no italics"): the Missions box and the phase screens
   drew the same card two ways. */
#app .card .block h5 { color: var(--ink); font-size: var(--t-micro); font-weight: 700; letter-spacing: var(--track-caps); }
#app .card .vp-read { color: var(--ink); font-size: var(--t-lead); font-weight: 700; }
#app .card [data-role="score"] { font-weight: 700; }
#app .card .score > i {
  display: inline-block; margin-right: 6px; padding: 2px 6px; border-radius: 5px;
  background: var(--inset); color: var(--ink-dim); font-size: var(--t-micro);
  font-style: normal; font-weight: 700; letter-spacing: var(--track-caps);
  text-transform: uppercase; vertical-align: 1px;
}
#app .card :is(.vp-read, [data-role="score"]) i { font-style: normal; }
/* A long chip's words wrap inside it rather than running past its edge ("Painted to a
   Battle Ready standard (01.11: 10VP)" in patrol step 3). */
#app [data-role="battle-ready"] { white-space: normal; text-align: left; height: auto; }

/* ---- 0.8.3, behaviour --------------------------------------------------------------- */
/* The attack ends where nothing comes into a step (26.39): the ready card's place, its
   header, and one line in the app's words at body size so it reads as the answer. */
#app .roll .ready-card .ends { margin: 12px 0 0; font-size: var(--t-body); color: var(--ink); }
#app .roll .ready-card.is-ended:not(.is-current) { border-color: var(--edge); }  /* 0.9.3: an ended attack with hazard rolls after it; K1 */
/* Three or four sides in one roll-off round (HR05.01): the tiles share the row. */
#app .play .rolloff .roll-tiles.is-many { grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); }
/* The army rule's own name in the Builder's box as T3, like the Plagues under it (0.8.3):
   it was the one name in the box left at the browser's weight, lighter than its entries. */
#app .army-rules > [data-army-rule] > .who { color: var(--ink); font-size: var(--t-lead); font-weight: 600; }
/* The image table's battle rounds are a table's first column, not names in a list (T2),
   so they take their own class since the faction page draws the table too (0.8.3). */
#app .image-table .round { flex: 1 1 auto; color: var(--ink); font-size: var(--t-body); font-weight: 600; }

/* ---- 0.8.4, the playtest of 2026-09-28 ---------------------------------------------- */
/* Three or four sides in a square grid, equal gaps (Ben: "Free for all, in a square grid
   with equal spacing"); two stay in halves. Overrides 0.8.3's auto-fit row. */
#app .play .rolloff .roll-tiles.is-many { grid-template-columns: repeat(2, 1fr); }
/* A side out of the round keeps its tile and its last die, dashed. */
#app .play .rolloff .roll-tile.is-out .die { border-style: dashed; }
/* Who decides, set apart (E1's accent border as the current thing). */
#app .play .decider { margin: 12px 0 0; padding: 10px 12px; border: 2px solid var(--accent);
  border-radius: 10px; display: grid; gap: 6px; }
#app .play .decider .label { color: var(--accent); margin: 0; }
#app .play .decider .decider-who { margin: 0; font-size: var(--t-lead); font-weight: 600; color: var(--ink); }
#app .play .decider .chips { margin: 2px 0 0; }
/* Two answers to one question in halves (the battlefield ask, Combat Patrol's built or printed). */
#app .choose-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 4px 0 8px; }
#app .choose-pair .chip { min-height: 36px; border-radius: 10px; }
#app .patrol-how { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 8px 0; }
#app .patrol-how .chip { min-height: 36px; border-radius: 10px; }
/* The Core Abilities that decide where a unit starts, on its formation row (0.8.4). */
#app .formation-name .deploy-abilities { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
/* The battlefield on the deploying steps (0.8.4). */
#app .play .battlefield-view { margin: 12px 0 0; display: grid; gap: 6px; }
/* End the battle, centred and clear of the foot (0.8.4, Ben: "Center the 'end the battle'
   button and space it out a bit further from the bottom of the scrolled extents"). */
#app .phase-screen [data-role="abandon"].danger { display: block; width: auto; min-width: 200px;
  margin: 32px auto 48px; padding: 0 24px; }

/* --- 0.8.5: the game screen, Ben's picks of the 0.8.4 mock-ups -------------------------- */
/* The card at the top: the round as its label, the title with its i, VP and CP on the
   left and Missions on the right in equal halves. */
#app .phase-head.phase-card {
  display: grid; gap: 8px; margin: 0 0 4px; padding: 12px 14px;
  background: var(--raised); border: 1px solid var(--line); border-radius: var(--radius);
}
#app .phase-card .phase-when { margin: 0; }
#app .phase-card > h1 { margin: 0; }
#app .phase-card .phase-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
#app .phase-card .phase-pair > button { width: 100%; min-height: 44px; }
/* The VP and CP button's face, pick B: the figures large, their names small, a hairline. */
#app .scores-face { display: inline-flex; align-items: baseline; gap: 14px; }
#app .scores-face > span { display: inline-flex; align-items: baseline; gap: 3px; }
#app .scores-face b { font-size: var(--t-display); line-height: 1; color: var(--ink); }
#app .scores-face small {
  font-size: var(--t-micro); font-weight: 700; letter-spacing: var(--track-caps); color: var(--ink-faint);
}
#app .scores-face .sep { width: 1px; align-self: stretch; background: var(--line); }
/* The VP and CP box. */
#app .dialog .score-line { display: grid; gap: 6px; padding: 10px 0; border-top: 1px solid var(--line); }
#app .dialog .score-line:first-of-type { border-top: 0; }
#app .dialog .score-line .name { margin: 0; font-size: var(--t-lead); font-weight: 600; }
#app .dialog .score-steps { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
#app .dialog .cp-of { display: grid; gap: 4px; }
#app .dialog .cp-of .label { margin: 0; }
#app .dialog .mini-stepper { display: grid; grid-template-columns: 44px 1fr 44px; gap: 6px; align-items: center; }
#app .dialog .mini-stepper > button { min-height: 44px; padding: 0; font-size: var(--t-lead); }
#app .dialog .mini-stepper > button:disabled { border-style: dashed; color: var(--ink-faint); }
#app .dialog .mini-stepper .value { display: grid; justify-items: center; }
#app .dialog .mini-stepper .value b { font-size: var(--t-display); line-height: 1.1; }
#app .dialog .mini-stepper .value span {
  font-size: var(--t-micro); font-weight: 700; letter-spacing: var(--track-caps); color: var(--ink-faint);
}
/* The foot: Stratagems and Army lists, then four equal buttons at one size. */
#app .phase-foot { position: fixed; }
#app .phase-foot .foot-four { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
#app .phase-foot .foot-four > button,
#app .phase-foot .foot-four > .primary { min-height: 48px; font-size: var(--t-lead); padding: 0 4px; }
#app .phase-foot .foot-row > button.on { border-color: var(--accent); }
/* The players behind each, opening upward. */
#app .phase-foot .foot-menu {
  position: absolute; bottom: calc(100% + 6px); width: calc(72% - 16px); display: grid; gap: 4px;
  padding: 6px; background: var(--raised); border: 1px solid var(--edge); border-radius: var(--radius-small);
  box-shadow: 0 -10px 26px rgba(0, 0, 0, 0.45); animation: foot-menu-up 0.16s ease-out;
}
#app .phase-foot .foot-menu.left { left: var(--gap); }
#app .phase-foot .foot-menu.right { right: var(--gap); }
@keyframes foot-menu-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
#app .phase-foot .foot-menu .foot-item {
  display: grid; grid-template-columns: 4px 1fr auto; gap: 10px; align-items: center; text-align: left;
  width: 100%; min-height: 44px; padding: 8px; border: 0; border-radius: 7px; background: transparent;
  color: var(--ink);
}
#app .phase-foot .foot-menu .foot-item:hover { background: var(--inset); }
#app .phase-foot .foot-menu .foot-item .bar {
  width: 4px; height: 28px; border-radius: 3px; background: var(--faction-colour, var(--edge));
}
#app .phase-foot .foot-menu .foot-item .who b { display: block; font-size: var(--t-body); }
#app .phase-foot .foot-menu .foot-item .who em {
  display: block; font-style: normal; font-size: var(--t-small); color: var(--ink-dim);
}
#app .phase-foot .foot-menu .foot-item .now-word { color: var(--accent); }
#app .phase-foot .foot-menu .foot-item .count { color: var(--ink); font-size: var(--t-body); margin: 0; }
#app .phase-foot .foot-menu .foot-item[style*="--faction-colour"] { background: transparent; border-color: transparent; }
#app .phase-foot .foot-menu .foot-item[style*="--faction-colour"]:hover { background: var(--inset); }
/* One and two dice outside the attack roller, bigger and slower (0.8.5, Ben: "roll a bit
   slower and be a bit bigger", and of the mock-up's 950ms tumble "maybe just a tad
   faster"): 64px dice, a 750ms drop, roll and settle, 200ms apart. The roller keeps its
   30px dice and 230ms quarter turn. */
#app :is(.play, .quick-roll) .die {
  width: 64px; height: 64px; padding: 9px; gap: 5px; border-width: 2px; border-radius: 14px;
}
#app :is(.play, .quick-roll) .revealed.is-fresh .rolling,
#app .play .roll-tile.is-fresh .rolling {
  display: inline-flex;
  animation: die-tumble 750ms cubic-bezier(.2, .8, .25, 1) backwards;
  animation-delay: calc(var(--at, 0) * 200ms);
}
@keyframes die-tumble {
  0%   { opacity: 0; transform: translateY(-26px) rotate(-200deg) scale(.7); }
  45%  { opacity: 1; transform: translateY(4px) rotate(30deg) scale(1.05); }
  65%  { transform: translateY(-6px) rotate(-12deg); }
  82%  { transform: translateY(0) rotate(5deg); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  #app :is(.play, .quick-roll) .revealed.is-fresh .rolling,
  #app .play .roll-tile.is-fresh .rolling { animation: none; }
}
/* The Advance and Charge rolls, centred. */
#app .quick-roll .dealt.big-dice { display: grid; justify-items: center; gap: 10px; margin: 6px 0 0; }
#app .quick-roll .quick-name { color: var(--ink-dim); font-size: var(--t-small); }
#app .quick-roll .quick-dice { display: flex; gap: 14px; justify-content: center; }
#app .quick-roll .total { font-size: var(--t-body); }
#app .quick-roll .total b { font-size: var(--t-display); }
#app .quick-roll .again { display: flex; justify-content: center; margin: 12px 0 0; }
/* --- 0.8.6 ------------------------------------------------------------------------- */
/* Which card to redraw or discard: a B9 row a card, the chosen in accent (E1). */
#app .dialog .card-pick { display: grid; gap: 8px; margin: 8px 0 12px; }
#app .dialog .card-pick > button.on { border-color: var(--accent); color: var(--ink); }
/* The cards held, the ones just drawn marked. */
#app .drawn-now .drawn-card { display: grid; gap: 4px; margin: 8px 0 0; }
#app .drawn-now .drawn-card > .badge { justify-self: start; }
/* A formation row: the name and its count on one line (0.8.6). */
#app .formations .formation-name > .name-line { display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 4px 8px; font-size: var(--t-body); color: var(--ink); }
#app .formations .formation-name > .name-line > b { display: inline; font-weight: 600; }
#app .formations .formation-name > .name-line > .detail { font-size: var(--t-small); color: var(--ink-dim); }
#app .formation-name .deploy-abilities .ability-link { text-decoration: none; color: var(--accent);
  border: 1px solid var(--line); }
/* The battle round, big and centred, at its start and end (0.8.6). */
#app .round-banner { display: grid; justify-items: center; gap: 2px; margin: 36px 0 28px; text-align: center; }
#app .round-banner .label { margin: 0; }
#app .round-banner .round-big { margin: 0; font-size: 2rem; font-weight: 700; line-height: 1.15; color: var(--ink); }
#app .round-banner .round-of { margin: 0; font-size: var(--t-lead); color: var(--ink-dim); }

/* --- 0.8.7 ------------------------------------------------------------------------- */
/* The players over the card, Ben's pick A of `mockup_turn_b4_086.html` ("I like A on
   all"). Each side an equal share of the row, its players splitting it, so in 2v1 the one
   player is as wide as the two. Filled in the faction's colour for the turn, dashed once
   played this round, struck through once eliminated. */
#app .turn-strip { display: grid; gap: 6px; margin: 0 0 6px; }
#app .turn-strip.has-teams { gap: 12px; }
#app .turn-side { display: grid; gap: 6px; min-width: 0; }
#app .turn-pill {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  border: 1px solid var(--line); border-radius: 999px; padding: 6px 6px; text-align: center;
  font-size: var(--t-body); font-weight: 600; color: var(--ink-dim); background: transparent;
}
#app .turn-strip.is-tight .turn-pill { font-size: var(--t-small); }
#app .turn-pill.is-now {
  border-color: var(--faction-colour, var(--edge)); color: var(--ink);
  background: color-mix(in srgb, var(--faction-colour, var(--edge)) 45%, var(--raised));
}
#app .turn-pill.is-done { border-style: dashed; color: var(--ink-faint); }
#app .turn-side.is-out .turn-pill { border-style: dashed; color: var(--ink-faint); text-decoration: line-through; }
#app .turn-note { margin: 0 0 8px; text-align: center; font-size: var(--t-small); color: var(--ink-dim); }
#app .turn-note b { color: var(--ink); }

/* --- 0.8.8 ------------------------------------------------------------------------- */
/* B5a, the roster row's three chips smaller (Ben, 2026-09-28: "Smaller chips", on 0.8.2's
   "roster rows heavier with three 36px chips"). Warlord, Configure and Remove are 30px
   drawn with 10px sides, so a long unit name wraps later; each still takes a 44px press
   through its ::after. */
#app .build-row > .chip.kill,
#app .build-row > .edit.chip,
#app .build-row > .tag.chip.warlord-chip { min-height: 30px; padding: 0 10px; }
#app .build-row > .chip.kill::after,
#app .build-row > .edit.chip::after,
#app .build-row > .tag.chip.warlord-chip::after { content: ""; position: absolute; inset: -7px -2px; }
/* Add models' steps of 5 (0.8.8, Ben: "D"): five controls on the row, so tighter gaps. */
#app .dialog .stepper-row.has-jump { gap: 8px; }
/* The loadout block's count by its first row, and the rider with a rule at its left edge
   (0.8.8, Ben's pick C: "C"). The count is the app's voice, dim (T9); the rider is the
   datasheet's, full ink (T4), and now also set apart by place. */
/* A long name, a price and the count do not always fit beside the stepper at phone width;
   the count then drops to a line of its own inside the cell, still right-aligned, and the
   price stays beside the name. */
#app .loadout .steppers .item { display: flex; flex-wrap: wrap; row-gap: 0; }
/* 0.9.28: the name, its i and its price hold together; a long name wraps inside itself
   (the name shrinks, the price does not), and the price stays at the name's end. */
#app .loadout .steppers .item-head { display: inline-flex; align-items: baseline; gap: 2px; min-width: 0; }
#app .loadout .steppers .item-head .item-name { min-width: 0; }
#app .loadout .steppers .block-count {
  margin-left: auto; padding-left: 8px; white-space: nowrap;
  color: var(--ink-dim); font-size: var(--t-small); font-variant-numeric: tabular-nums;
}
#app .loadout .why .bit[data-voice="quoted"] { border-left: 2px solid var(--edge); padding-left: 8px; }

/* --- 0.9.1 ------------------------------------------------------------------------- */
/* The list's QR code is a button since 0.9.1: tapped, it fills the screen (Ben,
   2026-09-29, approving it for heavy Onslaught lists). Drawn exactly as before. */
#app .qr-frame {
  display: block; padding: 0; border: 0; font: inherit; color: inherit; cursor: zoom-in;
}
#app .qr-frame:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* Two boxes that are the whole screen: the code full screen and the live scanner. The
   scrim loses its padding, the box its edges, and Back and the setup step bar (which float
   above every box) step aside, since the phone's own Back and the X both close it. */
:root .scrim:is([data-dialog="qr-full"], [data-dialog="scanner"]) {
  padding: 0; background: var(--ground);
}
:root .scrim:is([data-dialog="qr-full"], [data-dialog="scanner"]) .dialog {
  width: 100%; max-width: none; height: 100dvh; max-height: none;
  border: 0; border-radius: 0; box-shadow: none;
}
:root .scrim:is([data-dialog="qr-full"], [data-dialog="scanner"]) .dialog-head {
  padding-top: calc(12px + env(safe-area-inset-top, 0px));
}
body:has(.scrim:is([data-dialog="qr-full"], [data-dialog="scanner"])) :is(.back-float, .step-nav) {
  visibility: hidden;
}
/* The code as large as the box allows, square, white to its edges. */
:root .scrim[data-dialog="qr-full"] .dialog-body {
  flex: 1; display: flex; align-items: center; justify-content: center; padding: 0;
  background: #fff;
}
#app .qr-full {
  display: block; width: min(100vw, calc(100dvh - 80px)); max-width: 100%;
  padding: 0; border: 0; background: #fff; cursor: zoom-out;
}
#app .qr-full .qr { display: block; width: 100%; height: auto; }

/* The scanner: the camera's picture filling the width, a square guide over its middle
   (the part of each frame that is read), and one line of words under it. */
:root .scrim[data-dialog="scanner"] .dialog-body { flex: 1; display: flex; padding: 0; }
#app .scanner { flex: 1; display: flex; flex-direction: column; gap: 12px; padding: 0 0 16px; }
#app .scanner-view {
  position: relative; flex: 1; min-height: 0; overflow: hidden; background: #000;
}
#app .scanner-view video {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
}
#app .scanner-guide {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(78vw, 70%, 60dvh); aspect-ratio: 1; max-width: 100%;
  border: 2px solid #fff; border-radius: var(--radius-small);
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.35);
}
#app .scanner > :not(.scanner-view) { margin: 0 16px; text-align: center; }
/* Refused, or no camera: no picture to show, so no black box where it would be. */
#app .scanner:is([data-state="refused"], [data-state="none"]) .scanner-view { display: none; }
#app .scanner:is([data-state="refused"], [data-state="none"]) { padding-top: 16px; }

/* --- 0.9.2 ------------------------------------------------------------------------- */
/* The attack roll's D3s (0.9.2, Ben: "Rolling to determine attacks also does not show any
   dice"): a die with its 1 to 3 pips, captioned D3 under it. Phone and tablet alike. */
#app .roll .d3 { display: inline-grid; justify-items: center; gap: 2px; }
#app .roll .d3 > small { font-size: var(--t-micro); font-weight: 700; letter-spacing: 0.06em;
  color: var(--ink-faint); }

/* A weapon's count on a datasheet opened from a list (0.9.4), as the list's "(x5)". */
#app .weapons th .wcount { font-weight: 400; color: var(--ink-dim); white-space: nowrap; }

/* --- 0.9.4: the whole unit ---------------------------------------------------------- */
/* Every weapon declared, then each resolved in turn (Ben, of the mock-up: "Happy with the
   roller build it all."). The one-weapon roller's cards, steps and ready card, reused. */
#app .roll .weapon-row.whole-unit { border: 1px solid var(--edge); border-radius: var(--radius-small);
  background: var(--inset); margin: 0 0 12px; padding: 10px 12px; }
#app .unit-roll .unit-stages { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 10px;
  font-size: var(--t-micro); font-weight: 700; letter-spacing: var(--track-caps);
  text-transform: uppercase; color: var(--ink-faint); }
#app .unit-roll .unit-stages .on { color: var(--ink); }
#app .unit-roll .side-card + .side-card { margin-top: 10px; }
#app .unit-roll .who { display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  margin: 0 0 6px; padding: 0; border: 0; }
#app .unit-roll .who b { font-size: var(--t-lead); overflow-wrap: break-word; }
#app .unit-roll .dim { color: var(--ink-dim); font-size: var(--t-small); }
#app .unit-roll .unit-weapon { border-top: 1px solid var(--line); padding: 10px 0; }
#app .unit-roll .unit-weapon:first-child { border-top: 0; padding-top: 0; }
#app .unit-roll .wline { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; gap: 8px;
  align-items: center; }
#app .unit-roll .wline .wname { display: block; font-size: var(--t-lead); }
#app .unit-roll .wline .prof { display: block; font-size: var(--t-small); color: var(--ink-dim); }
#app .unit-roll .wline .kw { display: block; font-size: var(--t-micro); color: var(--ink-dim);
  letter-spacing: 0.04em; }
#app .unit-roll .tick { width: 26px; height: 26px; min-height: 0; padding: 0; border-radius: 5px;
  border: 2px solid var(--edge); background: transparent; display: inline-flex;
  align-items: center; justify-content: center; }
/* Ticked in the ink, on the inset: a filled square would read as a primary button (B1). */
#app .unit-roll .tick.on { background: var(--inset); border-color: var(--accent); }
#app .unit-roll .tick.on::after { content: ""; width: 10px; height: 6px; margin-top: -3px;
  border-left: 3px solid var(--accent); border-bottom: 3px solid var(--accent); transform: rotate(-45deg); }
#app .roll.unit-roll .stepper { display: inline-flex; align-items: center; gap: 0; margin: 0; height: 40px;
  border: 1px solid var(--line); border-radius: var(--radius-small); background: var(--inset); }
#app .roll.unit-roll .stepper button { width: 38px; height: 38px; min-height: 0; padding: 0; border: 0;
  background: transparent; color: var(--accent); font-size: var(--t-lead); font-weight: 700; }
#app .roll.unit-roll .stepper button[aria-disabled="true"] { color: var(--disabled-ink); }
#app .roll.unit-roll .stepper b { min-width: 30px; text-align: center; font-variant-numeric: tabular-nums; }
#app .unit-roll .wsub { display: grid; gap: 8px; margin: 8px 0 0 36px; justify-items: start; }
#app .unit-roll .wsub select { max-width: 100%; min-height: 40px; font-size: var(--t-body); }
#app .unit-roll .wsub .chips { margin: 0; }
#app .unit-roll .inline { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
#app .unit-roll .split-rows { display: grid; gap: 8px; }
/* 0.9.5: every stepper at the right of its row (Ben: "On the attack, the -/+ boxes are not
   right-hand justified"), the label taking the rest. */
#app .unit-roll .gedit .inline, #app .unit-roll .split-rows .inline { justify-content: space-between;
  flex-wrap: nowrap; }
#app .unit-roll .gedit .inline > span:first-child,
#app .unit-roll .split-rows .inline > span:first-child { flex: 1 1 auto; min-width: 0; }
#app .unit-roll .wsub { justify-items: stretch; }
#app .unit-roll .wsub > select, #app .unit-roll .wsub > .chip, #app .unit-roll .wsub > .chips { justify-self: start; }
#app .unit-roll .split-rows .inline > b { min-width: 30px; text-align: center; }
/* The weapon picked to go next, before its roll starts. */
#app .unit-roll [data-role="next-weapon"] .secondary.on { border-color: var(--accent); color: var(--accent); }
#app .unit-roll [data-role="next-weapon"] > .primary { width: 100%; margin: 12px 0 0; }
#app .unit-roll .first-target .chips { margin: 0; }
#app .unit-roll .result-line .wounded { flex-basis: 100%; }
/* Add rule: When, then What with its counter on the same row. */
#app .dialog .rule-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; }
#app .dialog [data-role="rule-profiles"] { margin: 0 0 6px; }
#app .dialog .rule-line { display: flex; gap: 8px; align-items: center; }
#app .dialog .rule-line > select { flex: 1 1 auto; min-width: 0; width: auto; }
#app .dialog .rule-line .stepper { display: inline-flex; align-items: center; height: 40px; flex: 0 0 auto;
  border: 1px solid var(--line); border-radius: var(--radius-small); background: var(--inset); }
#app .dialog .rule-line .stepper button { width: 38px; height: 38px; min-height: 0; padding: 0; border: 0;
  background: transparent; color: var(--accent); font-size: var(--t-lead); font-weight: 700; }
#app .dialog .rule-line .stepper button[aria-disabled="true"] { color: var(--disabled-ink); }
#app .dialog .rule-line .stepper b { min-width: 34px; text-align: center; font-variant-numeric: tabular-nums; }
#app .unit-roll .split-rows .wnotes { margin: 0; }
#app .unit-roll .unit-group-row { border-top: 1px solid var(--line); padding: 10px 0; }
#app .unit-roll .unit-group-row:first-of-type { border-top: 0; }
#app .unit-roll .gtitle { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
#app .unit-roll .gtitle b { font-size: var(--t-lead); }
#app .unit-roll .gtitle .order { display: inline-flex; width: 22px; height: 22px; margin-right: 6px;
  border: 1px solid var(--line); border-radius: 50%; align-items: center; justify-content: center;
  font-size: var(--t-micro); color: var(--ink-dim); }
#app .unit-roll .gstats { margin: 4px 0 8px 28px; font-size: var(--t-small); color: var(--ink-dim); }
#app :is(.unit-roll, .damage-result) .unit-pips { display: flex; flex-wrap: wrap; gap: 4px; margin: 0 0 0 28px; }
#app :is(.unit-roll, .damage-result) .unit-pips .pip { width: 16px; height: 16px; border-radius: 3px; border: 1px solid var(--edge);
  background: var(--inset); position: relative; }
#app :is(.unit-roll, .damage-result) .unit-pips .pip.char { border-radius: 50%; }
#app :is(.unit-roll, .damage-result) .unit-pips .pip.hurt::after { content: ""; position: absolute; inset: 3px;
  border-radius: 2px; background: var(--bad); }
#app :is(.unit-roll, .damage-result) .unit-pips .pip.dead { background: transparent; border-style: dashed;
  border-color: var(--line); }
#app .unit-roll .gedit { display: grid; gap: 8px; margin: 10px 0 0 28px; }
#app .unit-roll .gedit .inline > span:first-child { min-width: 190px; font-size: var(--t-body); }
/* A volley is a bordered group on the ground, so the step and ready cards inside it are
   not a raised card inside a raised card (S1). */
#app .unit-roll .unit-volley { margin: 10px 0 0; padding: 0 12px 12px; border: 1px solid var(--line);
  border-radius: var(--radius-small); background: transparent; }
#app .unit-roll .unit-volley.done .wnotes { margin: 0; }
#app .unit-roll .vhead { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline;
  gap: 4px 8px; padding: 10px 0; }
#app .unit-roll .vhead b { font-size: var(--t-lead); }
#app .unit-roll [data-role="pool-ask"] .secondary.wide,
#app .unit-roll [data-role="next-weapon"] .secondary.wide { width: 100%; margin: 8px 0 0; padding: 10px 12px;
  text-align: left; justify-content: flex-start; white-space: normal; font-size: var(--t-body);
  font-weight: 600; }
#app .unit-roll .unit-volley.done .pool-line { margin: 4px 0 0; }
#app .unit-roll .result-line { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 12px;
  padding: 8px 0; border-top: 1px solid var(--line); }
#app .unit-roll .result-line:first-child { border-top: 0; padding-top: 0; }
#app .unit-roll > .primary, #app .unit-roll > .secondary.wide,
#app .unit-roll .unit-targets > .wide { width: 100%; margin: 12px 0 0; }

/* --- 0.9.5: a list's datasheet header ------------------------------------------------ */
/* Ben, of `datasheet_header_mockup.html`: "I love option B. More room for the photo.",
   "Agree" to the same shape on the tablet, and the composition as "a single line unless
   bejng inspected". DESIGN.md section 32. */
/* No hairline between the composition and the stat line on a list's sheet ("Remove the
   thin hairline between unit composition and statline"). */
#app .sheet-read.is-roster .sheet-head { border-bottom: 0; padding-bottom: 0; }
#app .sheet-read .sheet-top { position: relative; }
#app .sheet-read .sheet-head { position: relative; z-index: 2; }
/* Under a photo the header is the photo's frame: the composition at its top and the stat
   line at its foot, one size with or without an INV. */
#app .sheet-read.has-photo .sheet-top { display: flex; flex-direction: column; justify-content: space-between;
  min-height: calc(var(--band-h, 0px) - 8px); z-index: 1; }
#app .sheet-read.has-photo .sheet-top > .statline-wrap .statline { margin-bottom: 8px; }
/* INV as a short tab under SV (option B), and its room kept under a photo. */
#app .statline .stat.invsv { display: flex; align-items: center; justify-content: center; gap: 6px;
  height: 26px; padding: 0 2px; }
#app .statline .stat.invsv dd { margin: 0; font-size: var(--t-body); }
#app .statline .inv-slot { grid-column: var(--stat-column, auto); height: 26px; }
/* The composition as a button under the box's title, opening a pop-up (0.9.13, Ben of
   `unit_composition_mockup.html`: "A Definitely"). DESIGN.md 32.4. It sits in the title
   bar's place, so the title bar's rule moves under it. */
#app .dialog:has(> .dialog-body > .unit-sheet > .comp-row) > .dialog-head { border-bottom: 0; }
#app .unit-sheet > .comp-row { margin: -12px -14px 12px; padding: 0 16px 10px;
  border-bottom: 1px solid var(--line); }
#app .comp-row .comp-button { min-height: 36px; padding: 0 12px; }
#app .comp-pop .label { margin: 4px 0 6px; }
#app .comp-pop .label + ul { margin-bottom: 12px; }
#app .comp-pop ul { list-style: none; margin: 0; padding: 0; }
#app .comp-pop li { padding: 7px 0; border-bottom: 1px solid var(--line); font-size: var(--t-body); }
#app .comp-pop li:last-child { border-bottom: 0; }
#app .comp-pop .published li { color: var(--ink-dim); }
/* The header drawn over the photo's frame while it is framed: dashed boxes on a light
   veil, the INV tab dotted because half of units have none. */
#app .header-guide { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
#app .header-guide .g { position: absolute; display: flex; align-items: center; justify-content: center;
  border: 1.5px dashed var(--ink); border-radius: 5px;
  background: color-mix(in srgb, var(--ground) 38%, transparent);
  color: var(--ink); font-size: var(--t-micro); font-weight: 700; letter-spacing: var(--track-caps);
  text-transform: uppercase; overflow: hidden; white-space: nowrap; }
#app .header-guide .g.soft { border-style: dotted;
  background: color-mix(in srgb, var(--ground) 20%, transparent); }
#app .dialog .banner-bar.is-below { position: static; margin: 10px 0 0; flex-wrap: wrap; }
#app .dialog .banner-bar.is-below .chip { background: var(--inset); }
#app .frame-window .header-guide { z-index: 1; }

/* The notes that came off the photo, on the ground above the weapon profiles. */
#app .sheet-notes { display: grid; gap: 6px; margin: 4px 0 8px; }
#app .sheet-notes:empty { display: none; }
#app .sheet-notes > * { margin: 0; }
/* The app's own words, T9, as they were in the masthead (the publisher's are T4). */
#app .sheet-notes .filtered { display: flex; align-items: center; gap: 6px;
  font-size: var(--t-small); color: var(--ink-dim); }
#app .sheet-notes .filtered .key { color: var(--ink-dim); }

/* --------------------------------------------------------------------------
   0.9.5: physical dice. Ben: "I would like an option to switch the process to manual
   rolls (physical dice) and have the player input successes as they step through", then
   "Counts; Hits and Criticals. Make them -/+ inputs", and of round two "do not show pips
   when representing physical dice. show just the dice outlines (orange for
   successes/fadedfor misses)". The switch sits on the setup; each count is a ready card
   of -/+ rows, the stepper at the right of its row as every roller stepper is.
   -------------------------------------------------------------------------- */
#app .roll .dice-switch { margin: 16px 0 0; }
#app .roll .dice-switch .chips { margin: 0; align-items: center; }
#app .roll .dice-switch .label { margin: 0 4px 0 0; }
#app .roll .dice-switch .wnotes { margin: 6px 0 0; }
#app .roll .dice-fixed { margin: 0 0 8px; }
#app .roll .table-card .need { margin: 8px 0 2px; }
#app .roll .table-card .count { margin: 0; }
#app .roll .table-card .aside { margin: 6px 0 0; }
#app .roll .table-count { display: flex; justify-content: space-between; align-items: center; gap: 8px;
  margin: 10px 0 0; }
#app .roll .table-count > span:first-child { flex: 1 1 auto; min-width: 0; }
#app .roll .table-count b { display: block; font-size: var(--t-body); }
#app .roll .table-count small { display: block; color: var(--ink-dim); font-size: var(--t-small); }
#app .roll .table-card .stepper { display: inline-flex; align-items: center; gap: 0; margin: 0; height: 40px;
  flex: 0 0 auto; border: 1px solid var(--line); border-radius: var(--radius-small); background: var(--inset); }
#app .roll .table-card .stepper button { width: 38px; height: 38px; min-height: 0; padding: 0; border: 0;
  background: transparent; color: var(--accent); font-size: var(--t-lead); font-weight: 700; }
#app .roll .table-card .stepper button[aria-disabled="true"] { color: var(--disabled-ink); }
#app .roll .table-card .stepper b { min-width: 30px; text-align: center; font-variant-numeric: tabular-nums; }
#app .roll .table-card > .primary { width: 100%; margin: 12px 0 0; }
/* A die counted at the table: its outline and no pips. A success in the accent, a critical
   filled where a rule uses it (P7, as the app's own dice), the rest faded in the ink. */
#app .die.is-shell { display: inline-block; padding: 0; background: transparent; }
#app .die.is-shell.is-on { border-color: var(--accent); border-width: 2px; }
#app .die.is-shell.is-dim { border-color: var(--ink-faint); }
#app .die.is-shell.is-crit { background: var(--accent); border-color: var(--accent); }

/* --------------------------------------------------------------------------
   0.9.5: the whole unit declared by target. Ben: "I like by target rather than by weapon
   - it feels more compact", then of round two: the state a pop-up, the profile in
   columns ("I like B"), a weapon's profiles each their own count ("Let it split").
   Each target a card in the order the units resolve; its state one line and its pips,
   tapped to set; under a heading of A, BS or WS, S, AP and D, each weapon's numbers in
   the same five columns and its count at the right of its row.
   -------------------------------------------------------------------------- */
#app .unit-roll .unit-target-card { margin: 0 0 12px; }
#app .unit-roll .unit-target-card .who .chip { margin: 0; }
#app .unit-roll .unit-target-card .led { margin: 2px 0 0; }
#app .unit-roll .state-line { display: flex; width: 100%; justify-content: space-between; align-items: center;
  gap: 10px; margin: 10px 0 0; padding: 8px 10px; min-height: 48px; text-align: left;
  background: var(--inset); color: var(--ink); border: 1px solid var(--line); border-radius: var(--radius-small);
  font: inherit; font-size: var(--t-small); }
#app .unit-roll .state-line > span:first-child { display: grid; gap: 4px; min-width: 0; }
#app .unit-roll .state-line .pip-row { display: flex; flex-wrap: wrap; gap: 4px; }
#app .unit-roll .state-line .unit-pips { margin: 0; }
#app .unit-roll .state-line .unit-pips .pip { width: 12px; height: 12px; }
#app .unit-roll .state-line .go { flex: 0 0 auto; color: var(--accent); font-weight: 700; }
#app .unit-roll .unit-target-card > .label { margin: 14px 0 2px; }
#app .unit-roll .pcolhead, #app .unit-roll .pcols { display: grid; grid-template-columns: repeat(5, 5.5ch);
  gap: 2px; text-align: center; font-variant-numeric: tabular-nums; }
#app .unit-roll .pcolhead { font-size: var(--t-micro); font-weight: 700; letter-spacing: .06em;
  color: var(--ink-dim); padding: 0 0 4px; border-bottom: 1px solid var(--line); }
#app .unit-roll .pcols { font-size: var(--t-body); color: var(--ink); margin: 2px 0 0; }
#app .unit-roll .ptags { display: flex; flex-wrap: wrap; gap: 4px; margin: 4px 0 0; }
#app .unit-roll .ptags span { font-size: var(--t-micro); color: var(--ink-dim); border: 1px solid var(--line);
  border-radius: 3px; padding: 1px 5px; letter-spacing: .04em; }
#app .unit-roll .tweapon { padding: 10px 0; border-bottom: 1px solid var(--line); }
#app .unit-roll .tweapon:last-child { border-bottom: 0; }
#app .unit-roll .twtop { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
#app .unit-roll .twtop > div:first-child { min-width: 0; }
#app .unit-roll .twtop .wname { display: block; font-size: var(--t-body); }
#app .unit-roll .twcount { display: grid; justify-items: end; gap: 2px; flex: 0 0 auto; }
#app .unit-roll .tweapon .of { font-size: var(--t-micro); color: var(--ink-dim); }
#app .unit-roll .pline { margin: 8px 0 0; }
#app .unit-roll .pname { display: block; font-size: var(--t-small); font-weight: 700; color: var(--ink-dim); }
/* A weapon given nothing here is named in the dim ink, never by opacity. */
#app .unit-roll .tweapon.zero .wname, #app .unit-roll .tweapon.zero .pcols,
#app .unit-roll .pline.zero .pcols { color: var(--ink-dim); }
#app .unit-roll .tweapon [data-role="weapon-rules"] { margin: 8px 0 0; }
#app .unit-roll .still-lines { display: grid; gap: 2px; margin: 6px 0 0; font-size: var(--t-small); }
#app .unit-roll [data-role="unit-still"] { margin: 12px 0; }
#app .unit-roll .unit-targets > .primary, #app .unit-roll .unit-targets > .secondary { margin: 4px 0 0; }
#app .dialog .unit-target { margin: 8px 0; }
#app .dialog [data-role="state-done"] { margin: 12px 0 8px; }
#app .unit-roll .unit-target-card .who .order { display: inline-flex; width: 22px; height: 22px; margin: 0 8px 0 0;
  border: 1px solid var(--edge); border-radius: 50%; align-items: center; justify-content: center;
  font-size: var(--t-micro); color: var(--ink-dim); vertical-align: 2px; }


/* The move to a new MFM release, said once on Forces (0.9.6). A card like a saved list's,
   because it is about the lists, and plain rows because every line is a sentence. */
.moved-card { border: 1px solid var(--line); border-radius: var(--radius); background: var(--raised);
  padding: 10px 12px; margin: 0 0 12px; }
.moved-card h2.section { margin: 0 0 6px; }
.moved-card .moved-list { margin: 8px 0 0; }
.moved-card .moved-list h3.label { margin: 0 0 4px; }
.moved-card .moved-changes { margin: 0; padding: 0 0 0 16px; font-size: var(--t-small); }
.moved-card .moved-changes li { margin: 2px 0; }
.moved-card [data-role="moved-done"] { margin: 10px 0 0; }
/* A selection the store cannot price (2026-10-01): its name and Remove on one line, the
   reason under both, full width. Not the six-column priced row, which it has no parts for. */
.build-row.unpriced { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
.build-row.unpriced .name { flex: 1 1 auto; min-width: 0; }
.build-row.unpriced .message { flex: 1 1 100%; margin: 0; }

/* ======================================================================================
   THE COLLECTION PASS, 0.9.9 (DESIGN.md section 37). Ben, 2026-09-29: "Some of the
   spacing for buttons and collapsible menus through the collection section are a bit
   strange. Recommend mockups throughout." His answers to `collection_pass_mockup.html`,
   2026-10-01, every one the recommendation, then "Build it as 0.9.9".

   One rhythm through the Collection's boxes: a label 6px over its control, a note 6px
   under it, one row of choices 8px from the next, one block 14px from the next.
   ====================================================================================== */
#app :is([data-dialog="add-models"], [data-dialog="record"], [data-dialog="bought"],
         [data-dialog="wishlist"]) .dialog :is(h3.label, h4.label) { margin: 14px 0 6px; }
#app :is([data-dialog="add-models"], [data-dialog="record"]) .dialog label.field {
  display: grid; gap: 6px; margin: 14px 0 0; }
#app :is([data-dialog="add-models"], [data-dialog="record"]) .dialog label.field:first-child,
#app :is([data-dialog="add-models"], [data-dialog="record"]) .dialog .shelf-line > .control > label.field { margin-top: 0; }
#app :is([data-dialog="add-models"], [data-dialog="record"]) .dialog .chips + .chips { margin-top: 8px; }
#app :is([data-dialog="add-models"], [data-dialog="record"]) .dialog :is(.chips, .field, .shelf-line) + .wnotes { margin-top: 6px; }
#app :is([data-dialog="add-models"], [data-dialog="record"]) .dialog .chips + label.field { margin-top: 14px; }
/* The record's sections at the dialogs' own 14px, and Remove 14px under History. */
#app [data-dialog="record"] .dialog .shelf-section { margin-top: 14px; padding-top: 14px; }
#app [data-dialog="record"] .dialog [data-role="remove-record"] { margin-top: 14px; }

/* One line: the label at the left, its control at the right (How many and its stepper,
   From and To in Move models), as the roller has it (DESIGN.md 33.2). */
#app .dialog .shelf-line { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin: 8px 0 0; }
#app .dialog h3.label + .shelf-line { margin-top: 0; }
#app .dialog .shelf-line > .key { flex: 0 0 auto; color: var(--ink-faint); font-size: var(--t-micro);
  font-weight: 700; letter-spacing: var(--track-caps); text-transform: uppercase; }
#app .dialog .shelf-line > .control { display: flex; justify-content: flex-end; flex: 1 1 auto; min-width: 0; }
#app .dialog .shelf-line > .control > .stepper-row { justify-content: flex-end; gap: 8px; margin: 0; }
#app .dialog .shelf-line > .control > label.field { width: 100%; max-width: 230px; margin: 0; }
#app .dialog .shelf-line > .control > label.field > select { width: 100%; }

/* Several loadouts at once: each a box, its count at the right of its first line. */
#app .dialog .loadout-box.is-many { margin: 0 0 8px; padding: 12px; border: 1px solid var(--line);
  border-radius: var(--radius-small); background: var(--raised); }
#app .dialog .loadout-box.is-many > .shelf-line:first-child { margin: 0 0 8px; }
#app .dialog .loadout-box > [data-role="remove-loadout"] { margin-top: 8px; }
#app .dialog .loadout-box + .actions.single { margin-top: 0; }

/* The note mark: the note symbol since 0.9.16, styled with it (`.note-mark.still`). */

/* Points per faction on the band, in the band's own white, under its name. */
#app .faction-fold > summary.faction-band { padding-block: 6px; }
#app .faction-fold > summary.faction-band > .title > .pts { display: block; font-size: var(--t-small);
  font-weight: 600; letter-spacing: 0; text-transform: none; }
#app .faction-fold > .points-notes { margin: 6px 0 0; }

/* On the painting table, and Add models 14px under whatever is last. */
#app .collection-foot { display: grid; gap: 0; }
#app ul.painting-rows { list-style: none; margin: 0; padding: 0; }
#app .painting-rows .record-row:last-child { border-bottom: 0; }
#app .painting-table .record-open > .n { font-size: var(--t-small); font-weight: 600; color: var(--ink); }
#app .painting-table > .wnotes { margin: 6px 0 0; }
#app .foot-side > [data-role="add-models"] { width: 100%; margin-top: 14px; }


/* Add rule's prompts from the unit's own rules (0.9.10, DESIGN.md section 38). Rows
   separated by hairlines inside the box, not cards (S1, S2): the rule's name and where it is from, Use at the right, the sentence quoted with
   its condition in the datasheet's own ink (T4). */
#app .dialog .rule-prompts { margin: 0 0 14px; padding: 0 0 14px; border-bottom: 1px solid var(--line); }
#app .dialog .rule-prompts > h3.label { margin-top: 0; }
#app .dialog .rule-prompt { margin: 0; padding: 10px 0; border-top: 1px solid var(--line); }
#app .dialog .rule-prompt .prompt-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
#app .dialog .rule-prompt .prompt-name { display: grid; gap: 2px; min-width: 0; }
#app .dialog .rule-prompt .prompt-name .src { color: var(--ink-dim); font-size: var(--t-small); font-weight: 400; }
#app .dialog .rule-prompt .quote { margin: 6px 0 0; color: var(--ink); font-size: var(--t-small); }
#app .dialog .rule-prompt .wnotes { margin: 6px 0 0; }
#app .dialog .rule-prompts > [data-role="prompts-more"] { margin-top: 8px; }

/* The sweep, the app's waiting mark (0.9.11, DESIGN.md section 40): the launcher icon in
   small, beside the words that say what the app is waiting on. Its sweep turns once every
   1.6 s, the loading screen's idle pace sped up for a mark this size. */
.sweep-mark { flex: none; display: block; }
.sweep-mark .sweep-turn { transform-box: view-box; transform-origin: 54px 54px; animation: sweep-turn 1.6s linear infinite; }
@keyframes sweep-turn { from { transform: rotate(-360deg); } to { transform: rotate(0deg); } }
@media (prefers-reduced-motion: reduce) { .sweep-mark .sweep-turn { animation: none; } }
#app .wnotes.waiting { display: flex; align-items: center; justify-content: center; gap: 8px; }
#app .rule-prompts .wnotes.waiting { justify-content: flex-start; }

/* --- 0.9.15: the Core Rules' worked-example diagrams on a rule ------------------------ */
/* Ben: "I would say this is definitely worth it. Please proceed". The book's picture,
   full width, its title and page under it in the app's small words; tapped, full size in
   a box that scrolls both ways. */
#app .diagrams { margin: 16px 0 0; display: grid; gap: 14px; }
#app .diagrams > .label { margin: 0; }
#app .diagram { margin: 0; display: grid; gap: 6px; }
#app .diagram-open { display: block; width: 100%; padding: 0; border: 0; background: none; cursor: zoom-in; }
#app .diagram img { display: block; width: 100%; height: auto; border-radius: var(--radius-small, 10px);
  border: 1px solid var(--line); }
#app .diagram figcaption { display: flex; justify-content: space-between; gap: 8px; flex-wrap: wrap;
  font-size: var(--t-small); }
#app .diagram figcaption .title { color: var(--ink); }
#app .diagram figcaption .source,
#app .scrim[data-dialog="diagram"] .source { color: var(--ink-faint); font-size: var(--t-small); margin: 8px 0 0; }
/* Fitted with a zoom since 0.9.18: the picture is drawn by .map-zoom (DESIGN.md 44). */
#app .diagram-full { display: block; }

/* **The note symbol, 0.9.16** (DESIGN.md 37.6 and 37.8). A page with a folded corner in
   the ink accent, in a 30px ring on the --edge line, tapped at 44 by the box around it.
   Directly after a unit's name (Ben: "directly after (to the right of) the unit name").
   In a Collection row it only marks, since the row is the button: no ring, dim ink. */
#app .note-mark { position: relative; display: inline-flex; align-items: center;
  justify-content: center; width: 30px; height: 30px; min-height: 30px; padding: 0;
  border: 1px solid var(--edge); border-radius: 50%; background: transparent;
  color: var(--accent); cursor: pointer; vertical-align: middle; flex: 0 0 auto; }
#app .note-mark::after { content: ""; position: absolute; inset: -7px; }
#app .note-mark svg { display: block; }
#app .note-mark.still { width: auto; height: auto; min-height: 0; border: 0;
  color: var(--ink-dim); cursor: inherit; margin: 0 0 0 6px; vertical-align: -2px; }
#app .note-mark.still::after { content: none; }
#app .build-row > .note-mark { grid-column: 2; grid-row: 1; justify-self: start; }
#app .summary-head .name > .note-mark { margin: 0 4px; width: 26px; height: 26px;
  min-height: 26px; vertical-align: -6px; }
#app .unit-note .note-text { margin: 0 0 14px; white-space: pre-wrap;
  font-size: var(--t-lead); line-height: 1.5; color: var(--ink); }
#app .unit-note label.field > textarea { width: 100%; box-sizing: border-box; }
#app .unit-note .wnotes { margin: 6px 0 0; }
#app .dialog-body > [data-role="remove"] { margin-top: 14px; }

/* **Edit at the right, 0.9.16** (C3): the row is the name, its note, its count, its points
   and Edit. The count moves to column 3 so the note can sit directly after the name. */
#app .build-row > .model-times { grid-column: 3; }
#app .build-row > .strength { grid-column: 3; }
#app .build-row > .edit { grid-column: 6; }

/* **A defender's leaders' rules, 0.9.17** (Ben: "Plain ones on, the rest switches"). A T6
   label over each kind; each rule its effect in ink, whose it is in the small dim words,
   and the publisher's sentence in ink (T4: rules text is full ink). */
#app .leader-rules { margin: 12px 0 0; display: grid; gap: 10px; }
#app .leader-rules > h4.label { margin: 4px 0 0; }
#app .leader-rule { display: grid; gap: 4px; }
#app .leader-rule .what { margin: 0; font-size: var(--t-body); }
#app .leader-rule .who { font-size: var(--t-small); color: var(--ink-dim); }
#app .leader-rule .quote { margin: 0; font-size: var(--t-small); color: var(--ink); }
#app .leader-rule .wnotes { margin: 0; }
#app .leader-rule > .chip { justify-self: start; }
/* A switch whose label is two effects ("Feel No Pain 4+ against mortal wounds; Feel No Pain
   4+ against Psychic Attacks", a Canoness's Null Rod) wraps rather than widening the card
   past a phone's screen (found 0.9.22; the chips are `nowrap` everywhere else). */
#app .leader-rule > .chip { max-width: 100%; white-space: normal; text-align: left; }

/* **A side's rules as one summary, since 0.9.23** (`DESIGN.md` 49; Ben: "T2, except make
   the 'why' a pop up from an 'i' symbol on the card"). The heading with its `i`, what is
   on as one line, the switches as chips that name their source, and the count of what is
   not applied. The working is in the `i`'s pop-up, drawn as before (43.1). */
#app .side-rules { margin: 12px 0 0; display: grid; gap: 8px; }
#app .side-rules-head { display: flex; align-items: center; gap: 2px; }
#app .side-rules-head > .label { margin: 0; }
#app .side-rules-on { margin: 0; font-size: var(--t-body); }
#app .side-rules-chips { display: flex; flex-wrap: wrap; gap: 8px; }
#app .side-rules-chips > .chip { max-width: 100%; white-space: normal; text-align: left; height: auto;
  min-height: 36px; display: inline-grid; align-content: center; gap: 2px; padding-block: 6px; }
#app .side-rules-chips > .chip .from { font-size: var(--t-small); font-weight: 400; color: var(--ink-dim); }
#app .side-rules > .wnotes { margin: 0; }

/* **A die waiting on its re-roll, since 0.9.23** (`DESIGN.md` 50; Ben: "R3, except you
   would always reroll so don't even provide the option to keep the failures"): ringed in
   the accent, dashed, while its first face shows; the button under the dice rolls it. */
#app .die.is-due { outline: 2px dashed var(--accent); outline-offset: 3px; }
/* **The dice grouped by what made them, since 0.9.24** (`DESIGN.md` 52; Ben: "Let's go
   C2"). One tray split in place: each group its T6 label over its dice, a keyword's
   label in the accent; the extra hits [SUSTAINED HITS X] made are dice with no face, a
   dashed accent outline holding +1, since they are not rolled. */
#app .step .dice.is-grouped { gap: 10px 16px; align-items: flex-end; }
#app .step .dice-group { display: grid; gap: 4px; justify-items: start; }
#app .step .dice-group > .label.is-keyword { color: var(--accent); }
#app .step .group-dice { display: flex; flex-wrap: wrap; gap: 8px; }
#app .die.is-ghost { display: grid; place-items: center; grid-template: 1fr / 1fr; padding: 0; background: transparent;
  border: 1px dashed var(--accent); color: var(--accent); font-size: var(--t-micro); font-weight: 700; }

#app .step .reroll-ask { display: grid; gap: 8px; margin: 10px 0 0 30px; }
#app .step .reroll-ask > .wnotes { margin: 0; }
#app .step .reroll-ask > .primary { min-height: 48px; }

/* **A map's key, 0.9.17** (Ben: "Check page 8 of the attached for terrain key"; "Key,
   roles and map in game"). The publisher's symbol at the left, 40px tall (an edge's line
   at its own width), its name in heading case and the page's words beside it. */
#app .map-key { margin: 12px 0 0; }
#app .map-key-rows { display: grid; gap: 10px; padding: 8px 0 0; }
#app .map-key-row { display: grid; grid-template-columns: 64px 1fr; gap: 12px; align-items: center; }
#app .map-key-row img { width: auto; height: 40px; max-width: 64px; object-fit: contain;
  border-radius: 3px; justify-self: center; }
#app .map-key-row.is-edges { grid-template-columns: 1fr; }
#app .map-key-row.is-edges img { height: auto; width: 100%; max-width: 260px; justify-self: start; }
#app .map-key-row .name { font-size: var(--t-lead); font-weight: 600; color: var(--ink); }
#app .map-key-row .message, #app .map-key-rows > .message { margin: 2px 0 0; }
#app .roles { display: grid; gap: 6px; margin: 12px 0 0; }
#app .roles .message { margin: 0; }
#app .missions-map { margin: 0 0 16px; }
#app .missions-map .deployment-map { width: 100%; height: auto; }
/* The card key's own symbol, before its name (Gameplay). */
#app .key-row .key-icon { display: block; height: 40px; width: auto; max-width: 96px;
  object-fit: contain; border-radius: 3px; margin: 0 0 6px; }

/* **Map pop-ups open fitted, 0.9.18** (DESIGN.md 44). Ben: "For all the map pop ups can you
   please fit to screen with the option to zoom instead of starting full scale like this?"
   The window is the picture's own shape, as wide as the box and no taller than the screen
   leaves room for; the picture is drawn inside it at any zoom from fitted to four times.
   Fitted, a finger still scrolls the box (pan-x pan-y leaves the pinch to the page's own
   code); zoomed, the finger moves the picture. */
#app .map-zoom-wrap { display: grid; gap: 4px; width: 100%; margin: 0 0 8px; }
#app .map-zoom {
  position: relative;
  overflow: hidden;
  width: min(100%, calc((100dvh - 300px) * var(--ratio, 1)));
  aspect-ratio: var(--ratio, 1);
  max-width: 100%;
  justify-self: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-small);
  touch-action: pan-x pan-y;
  cursor: zoom-in;
}
#app .map-zoom.is-zoomed { touch-action: none; cursor: grab; }
#app .map-zoom .map-zoom-img {
  position: absolute;
  display: block;
  height: auto;
  max-width: none;
  border: 0;
  border-radius: 0;
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}
#app .map-zoom-row { margin: 0; }
/* The layout pop-up's figure everywhere it opens: under Match-ups it had the browser's own
   40px figure margins, which is part of why it did not fit there. */
#app .dialog-body > figure[data-role="layout"] { margin: 0; }


/* **Hazard rolls and the result, 0.9.21** (DESIGN.md 47). Ben: "hazard rolls as gold is a
   little bit confusing - it appears as a success but actually it's a bad result", then
   "H1"; and "can we make X damage and X models destroyed a bit more punchy?", then "D3
   (except with D1's damage card)" and "make the number for 'destroyed' red to match".
   A failed hazard roll is a red die (K3's outline, with its word in the parts line) and
   its cost a red outline; the result's figures are 48px 700 (TYPOGRAPHY.md T15). */
#app .die.is-bad { border-color: var(--bad); background: var(--inset); }
#app .die.is-bad .pip.on { background: var(--bad); }
#app .die.is-shell.is-bad { border-color: var(--bad); border-width: 2px; background: transparent; }
#app .parts .part.is-bad, #app .parts .part.is-bad b { color: var(--bad); }
#app .roll .step .hazard-cost { margin: 10px 0 0 30px; padding: 10px 12px; border: 1px solid var(--bad);
  border-radius: var(--radius); font-size: var(--t-body); color: var(--ink); }
#app .roll .step .hazard-cost b { color: var(--bad); font-size: var(--t-figure); font-weight: 700; margin-right: 4px;
  font-variant-numeric: tabular-nums; }
#app .roll .step .damage-result { margin: 12px 0 0 30px; padding: 16px; background: var(--raised);
  border: 1px solid var(--line); border-radius: var(--radius); }
#app .roll .step .damage-result > .label { margin: 0 0 8px; }
#app .result-figs { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
#app .result-fig b { display: block; font-size: 3rem; line-height: 1; font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums; }
#app .result-fig span { display: block; margin-top: 6px; font-size: var(--t-micro); font-weight: 700;
  letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--ink-dim); }
#app .result-fig.is-destroyed b, #app .result-fig.is-destroyed span,
#app .roll .step .damage-result .result-fig.is-destroyed b,
#app .unit-roll .result-line .result-fig.is-destroyed b { color: var(--bad); }
#app .result-pips { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 0 4px; }
#app .damage-result .wnotes { margin: 0; }
#app .result-pips .unit-pips { margin: 0; }
#app .result-pips .unit-pips .pip.dead { border-color: var(--bad); position: relative; }
#app .result-pips .unit-pips .pip.dead::after { content: ""; position: absolute; inset: 2px;
  background: linear-gradient(45deg, transparent 44%, var(--bad) 44%, var(--bad) 56%, transparent 56%); }
#app .unit-roll .result-line.is-figures { display: block; }
#app .unit-roll .result-line.is-figures .result-name { display: block; margin: 0 0 8px;
  font-size: var(--t-lead); font-weight: 600; color: var(--ink); }

/* 0.9.25: a weapon note's name and its words are two runs, spaced (found testing build 57,
   "One ShotThe bearer can only shoot..."). */
#app .wnotes[data-role="weapon-note"] .who { margin-right: 6px; }
/* 0.9.25: in a Collection row the symbol sits 8px after the name, as the Builder's does
   (found testing build 57, the ring touching the name). */
#app span.note-mark[role="img"]:not(.still) { margin-left: 8px; }
/* A homebrew codex's Upgrades (0.9.27, DESIGN.md 54.4), on its faction page and its
   datasheets: each cost as an enhancement's is drawn (T12). */
#app [data-role="upgrades"] .who .cost,
#app [data-role="sheet-upgrades"] .who .cost {
  margin-left: 8px;
  color: var(--ink);
  font-size: var(--t-lead);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* 0.9.26: a homebrew army's V2 badge beside a name, spaced as the house rule badge is. */
#app .homebrew-tag:not(.shelf-tag) { margin-left: 6px; vertical-align: 2px; }
#app .masthead > .homebrew-tag { justify-self: start; align-self: start; width: max-content;
                                 margin-left: 0; }

/* ===================================================================================
   THE TABLET, 0.9.2
   Ben, 2026-09-29, on round 4 of the tablet mock-ups: "Build it". Everything below is
   inside one condition, width and height together, so a phone in either orientation is
   never a tablet (its long side is about 890px, its short side about 410). The phone's
   rules above are untouched. 0.9.2 was released after every phone screen was compared,
   pixel for pixel, with 0.9.1's; `tests/test_tablet_092.py` holds every rule below to
   this condition. Nothing but `@media` blocks with it may follow this comment.
   =================================================================================== */
@media (min-width: 840px) and (min-height: 600px) {
  :root { --panel-w: min(46vw, 680px); --main-w: calc(100vw - var(--panel-w) - 48px); }

  /* The app takes the screen, with a 24px gutter. A screen drawn for the tablet uses the
     width; any other sits on the left, and the right is the side panel's place (5a: "the
     screen stays put"), so opening a box never moves what you were reading. */
  #app { max-width: none; margin: 0; padding-left: 24px; padding-right: 24px; }
  .app-shell:not(.is-wide) .app-main { max-width: var(--main-w); }

  /* A box is a panel down the right, full height above the tab bar (5a). The screen
     under it is not dimmed; a tap on it closes the box, as a tap outside does on the
     phone. The code full screen and the scanner stay full screen (0.9.1). */
  :root .scrim:not([data-dialog="qr-full"]):not([data-dialog="scanner"]) {
    justify-content: flex-end; align-items: stretch;
    padding: 0 0 calc(var(--tabbar) + env(safe-area-inset-bottom, 0px)) 0;
    background: transparent;
  }
  :root .scrim:not([data-dialog="qr-full"]):not([data-dialog="scanner"]) .dialog {
    width: var(--panel-w); max-width: none; height: 100%; max-height: none;
    border-radius: 0; border-width: 0 0 0 1px;
    box-shadow: -12px 0 32px rgba(0, 0, 0, 0.35);
  }
  /* The X within the panel: on the phone it reaches 10px past the box's edge, which here
     is the screen's. */
  :root .scrim:not([data-dialog="qr-full"]):not([data-dialog="scanner"]) .dialog-head .close { margin-right: 0; }
  /* A box opened from inside a box sits over it, a little narrower, so both read. */
  :root .scrim .scrim:not([data-dialog="qr-full"]):not([data-dialog="scanner"]) .dialog {
    width: calc(var(--panel-w) - 40px);
  }

  /* **The tab bar under the left side only, and the panel to the foot, since 0.9.3.**
     Ben, the quick calls of 2026-09-29, 4: "Tablet the alternative". On a screen that
     sits on the left, the right is the panel's, so the bar ends where the panel begins
     and a box, or the list's panel, runs to the bottom of the screen. A screen drawn for
     the tablet uses the whole width and keeps the bar across it. */
  .app-shell:not(.is-wide) .tabbar { right: var(--panel-w); }
  .app-shell:not(.is-wide) .scrim:not([data-dialog="qr-full"]):not([data-dialog="scanner"]) {
    padding-bottom: 0;
  }
  #app .app-shell:not(.is-wide) .tablet-side { bottom: 0; }

  /* The turn strip, bigger (Ben, 2026-09-29, round 1: "agree on the bigger turn strip but
     don't rotate the screen at all"; TYPOGRAPHY T14). */
  #app .turn-pill { font-size: 19px; padding: 10px 8px; }
  #app .turn-strip.is-tight .turn-pill { font-size: var(--t-name-large); }
  #app .turn-note { font-size: var(--t-lead); }

  /* The phase title at 32px (round 4, "1 looks good"; TYPOGRAPHY T13, second use). */
  #app .phase-card h1[data-role="phase-title"] { font-size: 32px; line-height: 1.15; }

  /* The Collection across columns under one band per faction (5c, Ben: "break across
     multiple columns with a single banner for each faction across all columns"): the
     band the full width, the records under it in three columns in landscape, two in
     portrait. The needed-for-lists cards the same. */
  #app .faction-fold > ul.record-rows,
  #app .shelf-group[data-role="wanted"] > ul.records {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 24px;
  }
  /* A unit's banner across the columns, its records in columns under it (0.9.4). */
  #app .faction-fold > ul.record-rows > .unit-group { grid-column: 1 / -1; }
  #app .unit-group > ul.unit-records {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 24px;
  }
  #app .unit-records .record-row:last-child { border-bottom: 1px solid var(--line); }

  /* The list's panel (3a): the right-hand side, under any box that opens over it. Its
     z-index is 1, under the masthead's 5: a box opened from inside the masthead (Rename)
     is painted at the masthead's level, and a panel above 5 covered it (found drawing
     the 0.9.3 comparisons). */
  #app .tablet-side {
    position: fixed; top: 0; right: 0; bottom: calc(var(--tabbar) + env(safe-area-inset-bottom, 0px));
    width: var(--panel-w); z-index: 1; overflow-y: auto;
    padding: 20px 20px 24px; background: var(--ground); border-left: 1px solid var(--line);
  }
  #app .tablet-side .side-lines { list-style: none; margin: 0 0 18px; padding: 0; }
  #app .tablet-side .side-lines .line { display: flex; justify-content: space-between; gap: 12px;
    padding: 8px 0; border-bottom: 1px solid var(--line); }
  #app .tablet-side .side-lines .name { font-size: var(--t-lead); font-weight: 600; color: var(--ink); }
  #app .tablet-side .side-lines .cost { font-size: var(--t-lead); font-weight: 700; color: var(--ink); }
  #app .tablet-side .side-lines .total .name { font-weight: 700; }
  #app .tablet-side .side-lines .of { font-weight: 400; color: var(--ink-dim); }
  #app .tablet-side .finding { margin: 8px 0; }
  .app-shell[data-screen="build"] .findings-banner { right: var(--panel-w); }
  /* The list one column, as 3a drew it: the panel beside it takes the second. */
  .app-shell[data-screen="build"] .lines-group > .lines { columns: 1; }

  /* The game screen (round 3 and 4): the phone's column on the left, Missions, Stratagems
     and Army lists beside it, each with its own players' tabs and its own scroll. The
     strip runs across the top. */
  #app .game-grid { display: grid; grid-template-columns: 420px minmax(0, 1fr); gap: 16px;
    align-items: start; }
  #app .game-main { min-width: 0; }
  #app .tablet-cols { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px;
    position: sticky; top: 12px;
    height: calc(100dvh - var(--tabbar) - env(safe-area-inset-bottom, 0px) - 24px); }
  #app .tablet-col { display: flex; flex-direction: column; gap: 10px; min-height: 0;
    border: 1px solid var(--line); border-radius: var(--radius); padding: 12px; }
  #app .tablet-col > .label { margin: 0; }
  #app .tablet-col-body { flex: 1; min-height: 0; overflow-y: auto; }
  #app :is(.player-tabs, .column-tabs) { display: grid; gap: 6px;
    grid-template-columns: repeat(var(--tab-count, 3), minmax(0, 1fr)); }
  #app .tab-button { min-height: 40px; padding: 0 8px; border: 1px solid var(--edge);
    border-radius: var(--radius-small); background: transparent; color: var(--ink-dim);
    font-size: var(--t-lead); font-weight: 600; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
  #app .tab-button.on { border: 2px solid var(--accent); color: var(--ink); }
  #app .tablet-cols.is-portrait { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
  #app .game-grid.is-portrait { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  /* VP and CP the card's width, with Missions a column (round 4). */
  #app .game-grid .phase-card .phase-pair { grid-template-columns: minmax(0, 1fr); }
  /* The foot under the phone's column rather than across the screen. */
  #app .game-grid .step-nav.phase-foot { left: 24px; right: auto; width: 420px; }
  #app .game-grid.is-portrait .step-nav.phase-foot { width: calc(50vw - 32px); }

  /* The setup (rounds 2 to 4): the rail of steps on the left, the step on the right,
     Previous and Next under the step rather than across the screen. */
  #app .setup-grid { display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 24px;
    align-items: start; }
  #app .setup-rail { position: sticky; top: 12px; }
  #app .setup-rail ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
  #app .rail-step { border: 1px solid transparent; border-radius: var(--radius-small); }
  #app .rail-step.is-current { border-color: var(--accent); background: var(--raised); }
  #app .rail-go { display: flex; gap: 10px; align-items: center; width: 100%; min-height: 44px;
    padding: 6px 10px; border: 0; background: transparent; text-align: left;
    font-size: var(--t-body); font-weight: 600; color: var(--ink-dim); }
  #app .rail-step.is-current .rail-go { color: var(--ink); }
  #app .rail-step.is-done .rail-go { color: var(--ink-faint); }
  #app .rail-go .num { flex: 0 0 auto; display: inline-grid; place-items: center; width: 24px;
    height: 24px; border: 1px solid var(--edge); border-radius: 50%; font-size: var(--t-micro); }
  #app .setup-rail .seed { margin: 14px 0 0 10px; text-align: left; }
  #app .setup-main .step.is-current { border-top: 0; padding-top: 0; }
  #app .app-shell:is([data-screen="standard"], [data-screen="homebrew"], [data-screen="patrol"]) .step-nav {
    left: calc(24px + 280px + 24px); right: 24px; }

  /* A step with a card or a layout: the picture on the right at the screen's height, the
     words and controls on the left. The step's own wrappers step aside (`contents`) so
     the picture can take the right-hand column. */
  #app .setup-main .step.has-picture { display: grid; column-gap: 24px; align-items: start;
    grid-template-columns: minmax(0, 1fr) minmax(0, 46%); grid-template-rows: repeat(40, auto) 1fr; }
  #app .setup-main .step.has-picture > * { grid-column: 1; }
  #app .setup-main .step.has-picture > div:not([class]),
  #app .setup-main .step.has-picture > div:not([class]) > .revealed { display: contents; }
  #app .setup-main .step.has-picture :is(.deployment-map, .layouts-inline.is-chosen,
      .battlefield-view, .step-card):not(.battlefield-view *):not(.step-card *) {
    grid-column: 2; grid-row: 1 / -1; margin: 0; }
  #app .setup-main .step.has-picture :is(.deployment-map, .layouts-inline.is-chosen, .step-card,
      .battlefield-view) img { display: block; width: auto; max-width: 100%;
    max-height: calc(100dvh - var(--tabbar) - 200px); }
  /* Step 8: the card and the layout side by side (round 2). */
  #app .setup-main .step[data-step="8"].has-picture { grid-template-columns: minmax(0, 30%) minmax(0, 1fr); }
  #app .setup-main .battlefield-view { display: grid; grid-auto-flow: column; column-gap: 16px;
    grid-template-rows: auto auto 1fr; align-items: start; }
  #app .setup-main .battlefield-view img { max-height: calc(100dvh - var(--tabbar) - 240px); }
  #app .setup-main .battlefield-view > .layouts-inline { grid-row: 2 / span 2; margin: 0; }
  #app .setup-main .battlefield-view > .deployment-map { margin: 0; }
  /* The roll-offs bigger (rounds 3 and 4, steps 5 and 10). */
  #app .setup-main .play .rolloff .roll-tiles, #app .setup-main .rolloff .roll-tiles { max-width: 560px; }
  #app .setup-main .roll-tile { min-height: 180px; justify-content: center; }
  #app .setup-main .roll-tile .die { width: 96px; height: 96px; padding: 14px; gap: 6px;
    border-radius: 18px; }
  /* Two players side by side where the step is theirs (steps 1, 2 and 6). */
  #app .setup-main .play .sides, #app .setup-main .sides { display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
  #app .setup-main .sides > .size-chips, #app .setup-main .sides > :not(.side) { grid-column: 1 / -1; }
  /* Steps 9 and 11 (9c, 11c): who goes first, large, then who follows. Step 10's order. */
  #app .first-then { display: grid; gap: 12px; margin: 16px 0 0 30px; max-width: 560px; }
  #app .first-then-tile { border: 1px solid var(--line); border-radius: var(--radius);
    padding: 16px 20px; }
  #app .first-then-tile.is-first { border: 2px solid var(--edge); }
  #app .first-then-tile .label { margin: 0 0 4px; }
  #app .first-then-tile .who { margin: 0; font-size: 32px; font-weight: 700; color: var(--ink); }
  #app .round-order { display: grid; gap: 8px; margin: 16px 0 0 30px; max-width: 560px; }
  #app .round-order .label { margin: 0; }
  #app .round-order .order-row { display: flex; gap: 16px; align-items: center; padding: 12px 16px;
    border: 1px solid var(--line); border-radius: var(--radius); }
  #app .round-order .order-row.is-first { border: 2px solid var(--edge); }
  #app .round-order .order-row .n { font-size: var(--t-figure); font-weight: 700; color: var(--ink); }
  #app .round-order .order-row .who { font-size: var(--t-figure); font-weight: 700; color: var(--ink); }
  /* Step 5's choice as two full buttons (round 3). */
  #app .setup-main .decider .chips { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  #app .setup-main .decider .chips > button { min-height: 48px; font-size: var(--t-lead); }
  /* Step 6: the Fixed cards in full, under the player's tile. */
  #app .fixed-full { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); }
  #app .fixed-full .row-name { margin: 0 0 4px; }

  /* The roller, 4a (0.9.2). */
  #app .roll .setup { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
  #app .roll .setup > .side:not(.side-card) { grid-column: 1 / -1; }
  #app .roll-tablet { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
  #app .roll-tablet .roll-who { margin: 0; }
  #app .roll-tablet .who-name { display: block; font-size: var(--t-name-large); font-weight: 700;
    color: var(--ink); margin: 2px 0 4px; }
  #app .roll-tablet .roll-steps { display: grid; gap: 10px; align-content: start; }
  #app .roll-tablet .roll-next { position: sticky; top: 16px; }
  #app .roll-tablet .roll-next .ready-card { margin: 0; }
  #app .roll-tablet .step { border: 1px solid var(--line); border-radius: var(--radius);
    padding: 12px 14px; background: var(--raised); }
  #app .roll-tablet .step.is-live { border: 2px solid var(--edge); }
  #app .roll-tablet .step.is-todo { background: transparent; border-style: dashed; }
  #app .roll-tablet .step.is-todo h3 { color: var(--ink-faint); font-weight: 600; }
  #app .roll-tablet .step .result { font-size: var(--t-small); color: var(--ink-dim); }
  #app .roll-tablet .step .result b { color: var(--ink); font-size: var(--t-lead); }
  #app .roll-tablet .step.is-kept .dice { display: flex; }
  #app .roll-tablet .step.is-kept .die { width: 36px; height: 36px; padding: 5px; gap: 2px; }
  #app .roll-tablet .step .parts { margin-left: 30px; }

  /* The whole unit (0.9.4): the targets on the left and the volleys on the right, as the
     mock-up drew them. */
  #app .unit-roll .unit-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 24px; align-items: start; }
  #app .unit-roll .unit-cols > div > .primary { width: 100%; margin: 12px 0 0; }
}

@media (min-width: 840px) and (min-height: 600px) and (orientation: portrait) {
  #app .faction-fold > ul.record-rows,
  #app .shelf-group[data-role="wanted"] > ul.records,
  #app .unit-group > ul.unit-records { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* One column in portrait: the next card straight after the step just rolled, so the
     steps not reached yet are not drawn above it a second time. */
  #app .roll-tablet { grid-template-columns: 1fr 1fr; }
  #app .roll-tablet .roll-steps, #app .roll-tablet .roll-next { grid-column: 1 / -1; }
  #app .roll-tablet .roll-next { position: static; }
  #app .roll-tablet .step.is-todo { display: none; }
}

@media (min-width: 840px) and (min-height: 600px) {
  /* 0.9.5, Ben of the declare mock-up's round two, "excellent": the target's state opens
     over the left column, so the card being set stays in view on the right. */
  :root .scrim[data-dialog="target-state"]:not([data-dialog="qr-full"]):not([data-dialog="scanner"]) {
    justify-content: flex-start; }
  :root .scrim[data-dialog="target-state"]:not([data-dialog="qr-full"]):not([data-dialog="scanner"]) .dialog {
    border-width: 0 1px 0 0; box-shadow: 12px 0 32px rgba(0, 0, 0, 0.35); }
}

@media (min-width: 840px) and (min-height: 600px) {
  /* The tablet's foot in two columns: the painting table at the left; Add models one
     column wide at the right, above Needed for army lists, instead of a bar across the
     screen. */
  #app .collection-foot { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: 24px;
    align-items: start; margin-top: 22px; }
  #app .collection-foot > .painting-table { grid-column: 1; margin-top: 0; }
  #app .collection-foot > .foot-side { grid-column: 2; display: flex; flex-direction: column; }
  #app .foot-side > [data-role="add-models"] { order: -1; margin-top: 0; }
  #app .foot-side > .shelf-group[data-role="wanted"] { margin-top: 14px; }
  #app .foot-side > .shelf-group[data-role="wanted"] > ul.records { grid-template-columns: minmax(0, 1fr); }
}

/* The Damage step's result card on the tablet (0.9.21, DESIGN.md 47.2): the steps there sit
   on a raised card, so the result is drawn on no fill with its hairline (S1). */
@media (min-width: 840px) and (min-height: 600px) {
  #app .roll .step .damage-result { background: transparent; }
}
