/* DZC unit reference.
 *
 * Two rules hold throughout, and they are not negotiable:
 *   1. Gameplay text is NEVER clipped. No ellipsis, no nowrap on a name, a
 *      rule or a stat. Containers grow in height instead.
 *   2. Opening anything must not move the page. The rule popover is
 *      position:absolute at body level, so nothing behind it reflows.
 */

/* ── the tokens this file actually runs on ──────────────────────────────────
 *
 * Every one of these was written as `var(--surface-2, #fff)` and NEVER
 * DEFINED. A var() with a fallback and no definition is not a token, it is a
 * literal with extra steps. So the whole DZC surface was hardcoded light
 * while `--ink` (which app.css does define) dutifully flipped to near-white in
 * the dark theme. The result was white text on white cards across the builder,
 * the picker, the reference and Play Mode: a Light/Dark switch that had sat in
 * Settings the whole time and made the app unusable when you used it.
 *
 * Light values are the fallbacks that were already in force, so nothing about
 * the paper theme moves. Dark values map onto app.css's own dark ramp.
 */
:root {
  --surface-2: #fff;         /* a raised card */
  --surface-3: #f2ece0;      /* a tinted band inside one */
  --line: #d8d0c0;
  --ink-2: #6b6459;
  --danger: #b3261e;
  --acc-ink: #1b3a5c;        /* the ink numbers are set in */
  /* An issue panel, the pair to app.css's --warn-bg / --warn-ink. Those two
     exist for both themes; the red ones were hardcoded, so a note went dark
     olive on a dark page while an issue stayed a glaring block of pale pink
     and the two severities stopped looking like two severities. */
  --err-bg: #fde8e6;
  --err-ink: #7a1c15;
  /* "This gun belongs to one model, not the Squad." Jet's colour, given as
     f15d38 on 2026-08-07. A token because it is one idea in two places -- the
     variant chip on a weapon card and the variant edge in the weapon table --
     and they must not drift. */
  --variant: #f15d38;
  --variant-ink: #c33d1c;
  --font-ui: 'Jost', system-ui, sans-serif;
  --ease-deco: cubic-bezier(.33, 0, .67, 1);
}
html[data-theme="dark"] {
  /* Inverted: on paper the band is DARKER than the card, on dark it is
     lighter, because a band reads as a band by differing from the card it
     sits on and not by being any particular colour. */
  --surface-2: #232a33;
  --surface-3: #2c333d;
  --line: #353c45;
  --ink-2: #9aa0a8;
  --danger: #e2685a;
  --acc-ink: #74a7e0;
  --err-bg: #3a1f1c;
  --err-ink: #e2685a;
  /* Lifted off f15d38: the paper orange is a mid tone that goes muddy against
     a dark card, and this is the same hue with the value it needs there. */
  --variant: #ff8362;
  --variant-ink: #ff8362;
}
/* The one place --acc-ink is a background rather than ink. A light navy under
   white text is unreadable, so it takes the saturated navy app.css already
   uses for a primary button on dark. */
html[data-theme="dark"] .dzc-pick-add { background: #2e6db5; }

.dzc-wrap { max-width: 1440px; margin: 0 auto; padding: 8px 10px 48px; }

/* ── faction tabs ── */
.dzc-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.dzc-tab {
  font: 600 13px/1 var(--font-ui, 'Jost', system-ui, sans-serif);
  letter-spacing: .02em; padding: 9px 14px; border-radius: 6px; cursor: pointer;
  border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff);
  color: var(--ink, #24201c); transition: background .12s, border-color .12s, color .12s;
}
.dzc-tab:hover { border-color: var(--acc); }
.dzc-tab.is-active { background: var(--acc); border-color: var(--acc); color: var(--acc-on, #fff); }

/* ── toolbar ── */
.dzc-toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 8px; }
.dzc-search {
  /* An <input> does not inherit a font, so every search box in the app was
     rendering its placeholder in the UA's Arial while the rest of the screen
     was Jost. Wider, too, which is how the picker's placeholder came to be
     the thing that decides how much room the row has. */
  font-family: var(--font-ui, 'Jost', system-ui, sans-serif);
  flex: 1 1 280px; min-width: 0; padding: 9px 12px; font-size: 14px;
  border: 1px solid var(--line, #d8d0c0); border-radius: 6px;
  background: var(--surface-2, #fff); color: var(--ink, #24201c);
}
.dzc-search:focus-visible { outline: 2px solid var(--acc); outline-offset: 1px; }
.dzc-chips { display: flex; flex-wrap: wrap; gap: 5px; }
/* Deco pills: the corners are cut, not rounded. A 999px radius is the shape
   this project explicitly does not want, and a chamfer is the period-correct
   way to soften a rectangle. */
.dzc-chip {
  font: 500 12px/1 var(--font-ui, 'Jost', system-ui, sans-serif); padding: 7px 12px;
  border-radius: 0; cursor: pointer; border: 1px solid var(--line, #d8d0c0);
  background: transparent; color: var(--ink-2, #5b544a);
  transition: background .12s, color .12s, border-color .12s;
}
.dzc-chip:hover { border-color: var(--acc); color: var(--ink, #24201c); }
/* The fallback matters: --acc is declared inline on .dzc-wrap, and the picker
   modal lives outside it, so without one the active chip painted white on
   nothing and the filters looked broken. */
.dzc-chip.is-active {
  background: var(--acc, #1b3a5c); border-color: var(--acc, #1b3a5c); color: var(--acc-on, #fff);
}
/* One line of text had 22px of margin around it. */
.dzc-count { font-size: 12px; color: var(--ink-2, #6b6459); margin: 4px 0 10px; }

/* Force-organisation category, colour-coded.
   The four are what the whole of 3.2 turns on, Vanguard, Heavy and Support
   may not outspend Standard, and they were the same grey word on every card,
   every picker tile, every Squad row and the printed sheet. One ink each, set
   once here off data-cat, so the builder, the picker, the reference and the
   sheet cannot drift apart the way a JS map per file would.

   A tinted chip rather than coloured text: the category is a label, and
   colouring the word alone would make it look like a link. */
/* Two inks per category, and the split is the point. --cat is the swatch: the
   vivid colour, on a 7px square, which is what carries at a glance. --cat-ink
   is the WORD, darkened until it clears 4.5:1 on paper.

   Straight off the swatch colour the text came out at 2.7 to 3.6. Heavy,
   Vanguard and Standard all below AA. This app exists partly because
   TTCombat's own low-contrast printing is hard to read; shipping a category
   label nobody with tired eyes can read would be doing the same thing in a
   nicer typeface. Measured against the card, not guessed. */
/* THE COLOURS ARE GONE. Jet, 2026-08-07: "remove the colors for standard/
   vanguard/heavy/support bc they conflict with the other colors."
   
   They did. The app already spends colour on two things that are not
   negotiable, the six transport symbols, whose hue IS their meaning
   (3.2.4.2), and the faction accent, and a third scheme on the category word
   put five more hues on the same card competing with both. A category is a
   word; it can be a word. The tokens stay so the ratio bars and the army
   thumbnails still resolve, all on the neutral ink. */
[data-cat] { --cat: var(--ink-2, #6b6459); --cat-ink: var(--ink-2, #6b6459); }
.dzc-cat { display: inline-flex; align-items: center; font-weight: 600; color: var(--cat-ink); }
/* Dark theme: the darkened text inks go the wrong way on ink-coloured paper,
   so the word takes the vivid colour there and the swatch carries it too. */
[data-theme="dark"] [data-cat] { --cat-ink: var(--cat); }
.dzc-empty { padding: 32px 0; text-align: center; color: var(--ink-2, #6b6459); }

/* ── category groups ── */
/* 30px between category sections, with a headed rule already separating
   them, was two devices doing one job. */
.dzc-group { margin-bottom: 16px; }
.dzc-group-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px;
  font: 700 15px/1.2 var(--font-ui, 'Jost', system-ui, sans-serif);
  text-transform: uppercase; letter-spacing: .08em;
  border-bottom: 2px solid var(--acc); padding-bottom: 4px; margin-bottom: 8px;
}
.dzc-group-count { font-weight: 500; font-size: 12px; letter-spacing: 0; text-transform: none; color: var(--ink-2, #6b6459); }
.dzc-group-note { font-weight: 400; font-size: 12px; letter-spacing: 0; text-transform: none; font-style: italic; color: var(--ink-2, #6b6459); }

/* ── unit cards ── */
/* 300px, not 268: the stat grid inside each card needs two columns wide enough
   for "DAMAGE POINTS" without the label wrapping to shreds. */
.dzc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); gap: 8px; }
.dzc-card {
  display: flex; gap: 8px; padding: 7px; cursor: pointer;
  border: 1px solid var(--line, #d8d0c0); border-radius: 8px;
  background: var(--surface-2, #fff); transition: border-color .12s, transform .12s;
}
.dzc-card:hover { border-color: var(--acc); box-shadow: inset 0 0 0 1px var(--acc); }
.dzc-card:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
.dzc-card-art { flex: 0 0 auto; height: 72px; display: grid; place-items: center; }
.dzc-card-art img { max-width: 100%; max-height: 100%; object-fit: contain; }
.dzc-card-body { flex: 1 1 auto; min-width: 0; }
/* Names wrap. A unit name is gameplay text and is never cut off. */
.dzc-card-name { font: 700 15px/1.25 var(--font-ui, 'Jost', system-ui, sans-serif); margin: 0 0 4px; overflow-wrap: anywhere; }
.dzc-card-meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; font-size: 12px; color: var(--ink-2, #6b6459); margin-bottom: 6px; }
.dzc-points { font-weight: 700; color: var(--ink, #24201c); }
.dzc-pts { font-weight: 500; font-size: 10px; margin-left: 1px; color: var(--ink-2, #6b6459); }
.dzc-flag { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; padding: 3px 7px; border-radius: 3px; background: var(--surface-3, #f2ece0); color: var(--ink-2, #5b544a); }
.dzc-flag--rare { background: #c9a92c; color: #221c00; }
.dzc-flag--unique { background: #9c1818; color: #fff; }

/* ── stat pills ── */
.dzc-card-stats { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 6px; }
.dzc-stat { display: inline-flex; align-items: baseline; gap: 4px; padding: 2px 7px; border-radius: 4px; background: var(--surface-3, #f2ece0); }
.dzc-stat-k { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-2, #6b6459); }
.dzc-stat-v { font-size: 13px; font-weight: 600; }

/* ── transport badges ── */
.dzc-transport { display: inline-flex; align-items: center; gap: 3px; flex-wrap: wrap; }
/* ONE number per context.
   Every place that shows a badge used to set width, height and font-size
   separately, so the digit's size drifted against the shape's. And the
   capacity/cargo size difference set from JS did nothing at all, because
   `svg { inset: 0 }` stretched both back to the box. A context sets
   --badge-px now and everything else follows it.

   --cargo-px is the smaller solid. A solid that looks like it would drop into
   a hollow is the whole grammar of these symbols, so the two sizes travel
   together and neither is set on its own. */
.dzc-badge {
  --badge-px: 28px;
  --cargo-px: 22px;
  position: relative; display: inline-block;
  width: var(--badge-px); height: var(--badge-px);
}
.dzc-badge.is-cargo { width: var(--cargo-px); height: var(--cargo-px); }
.dzc-badge svg { position: absolute; inset: 0; width: 100%; height: 100%; }
/* Absolute + top/left % + translate(-50%,-50%), not grid place-items: the
   default (50%,50%) centres on the BOX, but a triangle's own area sits well
   off box-centre, so each shape below moves the anchor point, not the glyph
   directly. The translate always re-centres on whatever point that is. */
.dzc-badge-n {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  font: 700 calc(var(--badge-px) * .45) / 1 var(--font-ui, 'Jost', system-ui, sans-serif);
}
.dzc-badge.is-cargo .dzc-badge-n { font-size: calc(var(--cargo-px) * .45); }
.dzc-sep { font-weight: 700; font-size: 13px; color: var(--ink-2, #6b6459); padding: 0 1px; }
/* Exact area centroid of the SYMBOL.triangle / .triangle-down paths (dzc-
   units.js), as a % of the 24-unit viewBox: a triangle's mass sits 2/3 of the
   way from apex to base, not at the box's geometric middle. (2,21,21)/3=14.667
   → 61.11%; (22,3,3)/3=9.333 → 38.89%. Was an em-based translateY nudge that
   only ever approximated this and drifted a fraction of a px off at sizes
   where the font-size : box-size ratio wasn't exactly the base case's. */
.dzc-badge-triangle .dzc-badge-n { top: 61.11%; }
.dzc-badge-triangle-down .dzc-badge-n { top: 38.89%; }
/* The pentagon is not symmetric about its box either: two vertices at the
   waist and a flat base put its mass 12.517/24 down, not 12/24. It sat on the
   box centre with the square and the circle, which is a half-pixel at 22px and
   a visible drop at 66. Area centroid by the shoelace formula, same as the
   triangles. */
.dzc-badge-pentagon .dzc-badge-n { top: 52.15%; }

/* ── Art Deco pills ──
   The chip already carried the chamfer; everything else pill-shaped in the app
   was still a rounded rectangle, so the vocabulary only half existed. One
   variable, one clip, applied to every small label: Rare and Unique flags,
   rule keywords, variant chips, the faction chip and the stat cells.

   6px on a 12px-tall chip is a bevel; on a 20px cell it is a notch. The cut
   scales with the thing it is cutting rather than being one number everywhere. */
.dzc-flag,
.dzc-rule,
.dzc-pick-vchip,
.dzc-army-fac,
.dzc-wpn-up,
.dzc-shape-chip,
.dzc-view-toggle {
  border-radius: 0;
}
/* Stat cells are a grid, not a row of pills: cutting each one would make the
   block read as six loose chips instead of one table. Only the outer corners
   of the whole block are cut. */
.dzc-stats { border-radius: 0; }
/* Art and the capacity/cargo strip, one column down the left of the card.
   Fixed height on the strip so a Unit that prints no symbol leaves the same
   gap as one that does. The row is only worth having if it is in the same
   place on all 178 of them. */
/* align-self: start. The column holds art and the capacity strip and nothing
   else, so stretching it to the height of a taller body just made 59px of
   empty column on every card. */
.dzc-card-side { flex: 0 0 72px; align-self: start; display: flex; flex-direction: column; gap: 3px; }
.dzc-card-caps {
  display: flex; align-items: center; justify-content: center;
  gap: 3px; flex-wrap: wrap; min-height: 32px;
}
.dzc-caps-none { font-size: 13px; color: var(--ink-2, #9a9184); }
.dzc-stats .dzc-stat { border-radius: 0; }

/* Collection keeps panes: the running totals stay put while a 178-row list
   scrolls beside them. */
.dzc-coll-panes { display: grid; grid-template-columns: 230px minmax(0, 1fr); gap: 10px; align-items: start; }
.dzc-coll-rail { position: sticky; top: 12px; display: flex; flex-direction: column; gap: 8px; }
.dzc-coll-main { min-width: 0; }
@media (max-width: 900px) {
  .dzc-coll-panes { grid-template-columns: 1fr; }
  .dzc-coll-rail { position: static; flex-direction: row; flex-wrap: wrap; }
  .dzc-coll-rail .dzc-rail-card { flex: 1 1 200px; }
}

/* ── rule chips ── */
/* 11px was too small to read at arm's length and the 1px border round it read
   as a broken line rather than a box. 12.5px, more padding, and a border that
   is actually visible at that size. */
.dzc-rule {
  display: inline-block; margin: 2px 4px 2px 0; padding: 3px 9px; cursor: pointer;
  font: 500 12.5px/1.35 var(--font-ui, 'Jost', system-ui, sans-serif);
  border: 1px solid var(--acc); border-radius: 3px; background: transparent;
  color: var(--ink, #24201c);
  /* Rule names wrap rather than truncate, this is gameplay text. */
  white-space: normal; text-align: left; overflow-wrap: anywhere;
}
/* Underline on hover, not a fill. White on the faction accent was unreadable
   at 11px against half the palette. Shaltari orange and PHR gold especially. */
.dzc-rule:hover, .dzc-rule:focus-visible {
  background: transparent; color: var(--ink, #24201c);
  text-decoration: underline; text-underline-offset: 2px; text-decoration-thickness: 1.5px;
}
.dzc-rule--unknown { border-style: dashed; opacity: .7; }

/* Every rule the Unit uses, in full, under its weapon table. A chip with a
   tooltip is fine for reminding; it is no good for deciding, and on a phone
   there is no hover at all. Sharp, because a panel is square. */
.dzc-ruledefs {
  margin-top: 8px; display: grid; gap: 6px;
}
@media (min-width: 720px) { .dzc-ruledefs { grid-template-columns: 1fr 1fr; } }
.dzc-ruledef {
  padding: 7px 9px; border: 1px solid var(--line, #d8d0c0);
  background: var(--surface-2, #fff);
}
.dzc-ruledef h5 {
  margin: 0 0 2px; font: 600 12.5px/1.2 var(--font-ui, 'Jost', system-ui, sans-serif);
}
.dzc-ruledef h5 i { font-style: normal; font-weight: 400; color: var(--ink-2, #6b6459); }
.dzc-ruledef p { margin: 0; font-size: 12px; line-height: 1.45; }
.dzc-ruledef-src { display: block; margin-top: 3px; font-size: 10.5px; color: var(--ink-2, #6b6459); }
/* A rule named inside a sentence is not a chip. A chip mid-prose breaks the
   line it is in and reads as a label rather than as part of what you are
   reading, so it keeps the sentence's own type and takes a dotted underline.
   The same affordance the size in the rail uses. */
.dzc-rule--inline {
  display: inline; margin: 0; padding: 0; border: 0; border-radius: 0;
  font: inherit; color: inherit;
  text-decoration: underline dotted; text-underline-offset: 2px;
}
.dzc-rule--inline:hover, .dzc-rule--inline:focus-visible { text-decoration: underline; }

/* ── rule popover: absolute at body level, so the page never shifts ── */
.dzc-pop {
  position: absolute; z-index: 9000; max-width: min(360px, calc(100vw - 24px));
  padding: 12px 14px; border-radius: 8px;
  background: var(--surface-2, #fff); border: 1px solid var(--line, #d8d0c0);
  box-shadow: 0 8px 28px rgba(0, 0, 0, .18);
}
.dzc-pop h5 { margin: 0 0 6px; font: 700 13px/1.3 var(--font-ui, 'Jost', system-ui, sans-serif); }
.dzc-pop-alias { font-weight: 500; color: var(--ink-2, #6b6459); }
.dzc-pop p { margin: 0 0 8px; font-size: 13px; line-height: 1.5; }
.dzc-pop-src { font-size: 11px; color: var(--ink-2, #6b6459); }

/* ── detail sheet ── */
.dzc-detail-head { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 14px; }
.dzc-detail-art { flex: 0 0 160px; max-width: 160px; height: auto; object-fit: contain; }
.dzc-detail-head h3 { margin: 0 0 4px; font: 800 20px/1.2 var(--font-ui, 'Jost', system-ui, sans-serif); overflow-wrap: anywhere; }
.dzc-detail-meta { margin: 0 0 8px; font-size: 13px; color: var(--ink-2, #6b6459); }
.dzc-detail-rules { margin-bottom: 14px; }
/* The <h4> eyebrows these styled are gone; nothing in the app renders one. */
.dzc-variants ul { list-style: none; margin: 0 0 14px; padding: 0; }
/* No rule between variants: they are already separated by the space and by
   their own names. Half the divider bars in the app came out on 2026-08-07. */
.dzc-variants li { display: flex; justify-content: space-between; gap: 12px; padding: 3px 0; }

/* Wide tables scroll inside their own container; the page never scrolls sideways. */
.dzc-wpn { width: 100%; border-collapse: collapse; font-size: 12px; display: block; overflow-x: auto; }
.dzc-wpn th { text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2, #6b6459); padding: 5px 8px 5px 0; border-bottom: 1px solid var(--line, #d8d0c0); }
.dzc-wpn td { padding: 6px 8px 6px 0; border-bottom: 1px solid var(--line, #eee5d6); vertical-align: top; }
.dzc-wpn-name { font-weight: 600; min-width: 180px; overflow-wrap: anywhere; }
.dzc-wpn tr.is-upgrade .dzc-wpn-name { border-left: 3px solid #2e9e4f; padding-left: 6px; }
.dzc-wpn tr.is-variant .dzc-wpn-name { border-left: 2px solid var(--variant, #f15d38); padding-left: 6px; }
.dzc-wpn-only, .dzc-wpn-up { display: block; font-weight: 500; font-size: 11px; color: var(--ink-2, #6b6459); }
.dzc-none { font-size: 12px; color: var(--ink-2, #6b6459); }

/* ── a weapon as a card ──
   The eight-column table wanted 620px and got it by scrolling sideways inside
   its own box, so on a phone the Special column. The one carrying the rules.
   Was the one you never saw without dragging. Same eight fields, laid out to be
   read.

   auto-fill with a 232px floor: FOUR across on a desktop pane, three on a
   narrow one, two on a tablet, one on a phone, without a breakpoint anywhere.
   The pane is 970px, so four is 232px a card and every measurement below is
   spent buying that: the arc is 38px rather than 46, the padding and both
   gaps are a couple of pixels tighter, and the value column is 2.7em instead
   of 3.4. Nothing is dropped. The card carries the same eight fields it did
   at three across. */
/* A TABLE, not a row of cards. Jet, 2026-08-07: "those weapons shouldn't be
   cards, they should be a table. a vertical table. as in no space between."
   
   So: no gap, and every weapon after the first drops its top border onto the
   one above. They butt together into a continuous block the way table rows do,
   and the grid still gives more than one column where there is room for it. */
/* minmax(MIN(232px, 100%), 1fr) -- and the min() is the whole of a real bug.

   A bare minmax(232px, 1fr) is a FLOOR, not a preference: in a container
   narrower than 232px the track is still laid out at 232 and the difference
   runs off the side. Measured on the mobile Group detail at a 320px viewport,
   where a carrier's weapon block gets 170px: the page scrolled sideways by 62,
   which is 232 - 170 exactly.

   The layout harness could not see it, because it never drills into a Group --
   the one screen used at a table. NOTES.md records a horizontal-overflow bug
   reaching a real phone once already.

   min(232px, 100%) makes the floor "232, or the whole container if that is
   less". Above 232 nothing changes at all; below it the track fits instead of
   overflowing. Applied to every auto-fill grid in this file for the same
   reason -- each one is a floor somebody will eventually be narrower than. */
.dzc-wcards {
  display: grid; gap: 0;
  grid-template-columns: repeat(auto-fill, minmax(min(232px, 100%), 1fr));
}
.dzc-wc {
  border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff);
  padding: 3px 6px; min-width: 0; margin: -1px 0 0 -1px;
}
.dzc-wc-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; }
.dzc-wc-name {
  margin: 0; font: 700 13px/1.2 var(--font-ui, 'Jost', system-ui, sans-serif);
  overflow-wrap: anywhere; color: var(--ink, #24201c);
}
/* WHICH MODEL CARRIES THIS GUN. A chip with an arrow, not a rule down the
   edge of the card. Jet, 2026-08-07: "the orange dashed line thing is too
   distracting. Thinner. This color. f15d38. actually just make it the tiny
   arrow-ish chip instead."

   The bar it replaces was 3px of orange down the whole left edge of every
   variant-restricted card, which is a lot of ink to say one word, and it said
   it in a place with no words in it -- you still had to find "Sabre only" in
   the header to learn WHICH model. The chip is both at once, where the name
   is, at the size of the thing it is: a footnote on a heading. */
.dzc-wc-only {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 0 5px 0 4px; border: 1px solid var(--variant, #f15d38);
  color: var(--variant-ink, var(--variant, #f15d38));
  font: 600 10px/16px var(--font-ui, 'Jost', system-ui, sans-serif);
}
.dzc-wc-only-i { flex: none; }

/* Energy is the one stat you can press, so it has to look pressable without
   becoming a button in a row of numbers: same type, same place, an underline
   that says there is something under it. */
.dzc-wc-e {
  padding: 0; border: 0; background: none; cursor: pointer; text-align: left;
  /* The same type as every other value on the card -- it reads as a number
     that happens to be pressable, not as a control that happens to be here. */
  font: 600 13px/1.1 var(--font-numeric, 'Outfit', sans-serif);
  font-variant-numeric: tabular-nums lining-nums; color: var(--ink, #24201c);
  text-decoration: underline; text-decoration-style: dotted;
  text-underline-offset: 2px; text-decoration-color: var(--line, #d8d0c0);
}
.dzc-wcards--marked .dzc-wc.is-off .dzc-wc-e { color: var(--ink-2, #6b6459); font-weight: 500; }
.dzc-wcards--marked .dzc-wc.is-swapped .dzc-wc-e { text-decoration: line-through; color: var(--ink-2, #6b6459); }
.dzc-wc-e:hover, .dzc-wc-e:focus-visible { text-decoration-color: var(--acc, #1b3a5c); }

/* The Energy vs Armour table, in the popover (6.2.4). Eleven columns of two
   characters, so it fits a phone without scrolling sideways. */
.dzc-pop--dmg { max-width: min(94vw, 420px); }
.dzc-dmg { border-collapse: collapse; margin: 4px 0 6px; width: 100%; }
.dzc-dmg th, .dzc-dmg td {
  padding: 2px 3px; text-align: center; font-weight: 600;
  font-family: var(--font-numeric, 'Outfit', sans-serif); font-size: 11px;
  border: 1px solid var(--line, #d8d0c0);
}
.dzc-dmg-k {
  text-align: left; white-space: nowrap; font-size: 10px;
  font-family: var(--font-ui, 'Jost', sans-serif); color: var(--ink-2, #6b6459);
}
.dzc-dmg tr:first-child th { background: var(--surface-3, #f2ece0); }
.dzc-dmg-crit td { color: var(--variant, #f15d38); }
.dzc-wc-body { display: flex; align-items: flex-start; gap: 6px; margin-top: 2px; }
/* The arc, drawn once at a size that reads, with its own code under it. */
.dzc-wc-arc {
  flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 2px;
  font-size: 10.5px; font-weight: 600; color: var(--ink-2, #6b6459);
}
/* Two columns, and the label sits ON TOP of its number rather than across a
   gap from it.
   
   Side by side, a pair costs the width of the longest label PLUS the widest
   value PLUS the rule of whitespace between them, and the whitespace was most
   of it: "MOVE & ATTACK ..... Full" is two short things and an inch of nothing.
   Stacked, a pair costs the wider of the two, which took the card from 319px
   to 232 and is what makes four fit across the pane instead of three. Nothing
   shrank to do it; the labels went back UP to 10px and the values to 14px,
   because the room came out of the gap.
   
   The values still line up card to card: the columns are equal fractions of
   the same box, so the second stat starts at the same x on every one. */
/* THREE COLUMNS, NOT TWO. Jet, 2026-08-07: "cut down on the vertical height
   for all the gun shit? Maybe by making it 3 columns and font shit?"
   Five stats in two columns is three rows; in three columns it is two. That is
   a whole row of label-plus-value off every weapon card in the app, and there
   are eight of them on a Howitzer. The type comes down with it -- see below --
   so the columns still hold their numbers without wrapping. */
.dzc-wc-stats {
  flex: 1 1 auto; min-width: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 6px;
}
.dzc-wc-stat { display: flex; flex-direction: column; min-width: 0; }
/* The one icon that keeps its own colours sits INSIDE its label, so it reads
   as part of the word rather than as a second mark beside it. */
/* ONE LINE, ALWAYS. In a 57px column "M&A MAX", "ATTACKS" and "RANGE" wrapped
   and their cells came out 39px tall against 25 for the two that fitted -- so
   the grid was 64px where 50 would do, on every weapon card in the app. The
   label is the one thing here that can afford to be smaller: it is a fixed
   word you learn once, sitting directly above the number you actually read. */
.dzc-wc-k {
  font: 700 8.5px/1.15 var(--font-condensed, 'Jost', sans-serif);
  text-transform: uppercase; letter-spacing: 0; color: var(--ink-2, #6b6459);
  white-space: nowrap; overflow-wrap: normal; word-break: keep-all;
}
/* The die sits with the number, not beside it: "5" and the cube are one value. */
.dzc-wc-v svg { vertical-align: -2px; margin-left: 2px; opacity: .75; }
.dzc-wc-v {
  font: 600 13px/1.1 var(--font-numeric, 'Outfit', sans-serif);
  font-variant-numeric: tabular-nums lining-nums; color: var(--ink, #24201c);
}
.dzc-wc-rules { margin-top: 2px; }
/* The same three marks the table carries: an upgrade, a variant-only gun, and
   in a Squad whether this one is actually yours. */
/* No edge on an upgrade card either. The price button in its corner says
   "+10pts" or "Bought" in the same green, an inch away. */
/* The weapon CARD says it with the chip beside the name instead -- see
   .dzc-wc-only. Nothing is left on the card's edge but the upgrade mark, which
   is a different question ("can I buy this") and the only one left needing an
   edge. */
.dzc-wcards--marked .dzc-wc.is-live { background: var(--surface-3, #f2ece0); }
/* A gun this Squad does NOT have. It used to differ from one it does by a
   slightly lighter ink on two lines, which at a glance was no difference at
   all -- the question the card answers is "is this mine", and the answer has
   to carry across a table. Faded, desaturated and on a dashed edge, so the
   whole card reads as inactive before you read a word of it. Still fully
   legible: it is what you are deciding whether to buy. */
/* Greyed, not ERASED. Jet, 2026-08-07: "undo what you just did with the guns.
   they're GONE now."
   
   A Battle Bus is a one-model Squad whose every gun belongs to a Variant, so
   on the default Bus every weapon on the card is `is-off` at once. At .58
   opacity, desaturated, on a transparent background, with the gaps between
   cards just removed and the padding halved, the whole block dissolved into
   the paper -- the guns had not moved, they had stopped being visible. The
   background stays, the filter goes, and the fade is much lighter: still
   plainly not yours, still plainly there. */
.dzc-wcards--marked .dzc-wc.is-off {
  opacity: .82; border-style: dashed;
}
.dzc-wcards--marked .dzc-wc.is-off .dzc-wc-name,
.dzc-wcards--marked .dzc-wc.is-off .dzc-wc-v { color: var(--ink-2, #6b6459); font-weight: 500; }
.dzc-wcards--marked .dzc-wc.is-swapped { opacity: .7; }
.dzc-wcards--marked .dzc-wc.is-swapped .dzc-wc-name,
.dzc-wcards--marked .dzc-wc.is-swapped .dzc-wc-v { text-decoration: line-through; color: var(--ink-2, #6b6459); }

/* The moment a gun becomes yours.
   
   Buying an upgrade or taking a Variant redraws the whole Squad, so the card
   you were looking at is replaced by an identical one that happens to be
   live -- the change you paid for happened off-screen. This is that change,
   said once: the edge takes the faction accent and a wash runs off the card
   over three quarters of a second.
   
   Nothing moves. No lift, no slide, no scale -- the card stays exactly where
   it was and only its colour reports. */
@keyframes dzc-gained {
  0%   { box-shadow: inset 0 0 0 2px var(--acc, #1b3a5c); background: color-mix(in srgb, var(--acc, #1b3a5c) 18%, transparent); }
  70%  { box-shadow: inset 0 0 0 2px var(--acc, #1b3a5c); background: color-mix(in srgb, var(--acc, #1b3a5c) 18%, transparent); }
  100% { box-shadow: inset 0 0 0 0 transparent; background: var(--surface-3, #f2ece0); }
}
.dzc-wcards--marked .dzc-wc.is-gained { animation: dzc-gained .75s var(--ease-out, ease-out) 1; }
@media (prefers-reduced-motion: reduce) {
  .dzc-wcards--marked .dzc-wc.is-gained { animation: none; box-shadow: inset 0 0 0 2px var(--acc, #1b3a5c); }
}

/* A Behemoth's Gear. Priced in Power out of the pool it spends to act, so it
   is a price list to read mid-game rather than anything to buy. No buttons,
   and it sits with the weapons because that is what it is read against. */
.dzc-gear { margin-top: 10px; }
.dzc-gear-head {
  display: block; font-size: 11px; font-weight: 600;
  color: var(--ink-2, #6b6459); margin-bottom: 4px;
}
/* A column of prices, not a wrapped row of them. Four numbers a player is
   choosing between should be readable down the left edge; in a flex wrap they
   landed at four different x and could only be compared by reading. */
.dzc-gear-list {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); gap: 3px 14px;
}
.dzc-gear-list li {
  display: grid; grid-template-columns: 34px minmax(0, 1fr);
  align-items: baseline; gap: 6px; font-size: 12px;
}
.dzc-gear-pt {
  font: 700 13px/1 var(--font-numeric, 'Outfit', sans-serif);
  font-variant-numeric: tabular-nums lining-nums;
  min-width: 2.2em; text-align: right; color: var(--ink, #24201c);
}
.dzc-gear-pt small { font-size: 9.5px; font-weight: 600; margin-left: 1px; color: var(--ink-2, #6b6459); }

/* A Squad's table prints the whole card and marks the guns that Squad actually
   fires. The variants its models are, the upgrades it bought. The rest stay
   on the page and go quiet.

   Quiet, not hidden: --ink-2 is a real colour on paper, and every number on a
   dead row is still there to be read and compared against the live one above
   it. That is the whole point of showing it. Opacity is deliberately not used:
   it would take the row below the contrast floor. */
.dzc-wpn--marked tr.is-off td { color: var(--ink-2, #6b6459); }
.dzc-wpn--marked tr.is-off .dzc-wpn-name { font-weight: 500; border-left-color: var(--line, #d8d0c0); }
.dzc-wpn--marked tr.is-off .dzc-rule { color: var(--ink-2, #6b6459); border-color: var(--line, #d8d0c0); }

/* Both ends of it, not just the quiet one.
   Dimming alone left the difference to a shade of grey, and "which of these
   nine rows are mine" is the question the table exists to answer. It should
   be answerable at arm's length, not by comparing two greys. So the live rows
   are lit as well: a tinted band the whole row wide, and the name at full
   weight and full ink. The dead ones keep the bare paper.

   Deliberately NOT a coloured left bar. That edge already carries the card's
   own vocabulary, green is an upgrade, orange is variant-only, and on UCM
   the faction accent is the same green as the upgrade bar, so "this row is
   live" and "this row is an upgrade" would have been the same mark. The band
   says live; the bar keeps saying what kind of row it is. */
.dzc-wpn--marked tr.is-live td { background: var(--surface-3, #f2ece0); }
.dzc-wpn--marked tr.is-live .dzc-wpn-name { font-weight: 700; color: var(--ink, #24201c); }

/* A gun a bought swap traded in. "May replace both its MC-20 Chainguns with
   MM-15 Sidearm Missiles". Struck through, not dimmed: a dead variant row is a
   gun this Squad could have had, and this is one the card prints that the
   purchase above has taken away. Two different sentences, two different marks.
   The row stays, because what you gave up is worth seeing next to what you
   got. */
.dzc-wpn--marked tr.is-swapped td { color: var(--ink-2, #6b6459); text-decoration: line-through; }
.dzc-wpn--marked tr.is-swapped .dzc-wpn-name { font-weight: 500; border-left-color: var(--line, #d8d0c0); }
.dzc-wpn--marked tr.is-swapped .dzc-rule { text-decoration: line-through; color: var(--ink-2, #6b6459); border-color: var(--line, #d8d0c0); }

@media (max-width: 640px) {
  .dzc-wrap { padding: 10px 12px 56px; }
  .dzc-grid { grid-template-columns: 1fr; }
  .dzc-detail-art { flex-basis: 110px; max-width: 110px; }
}

/* ═══ Army list ═══ */
.dzc-list-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.dzc-list-head h1 { font: 800 24px/1.2 var(--font-display, 'Roboto Slab', serif); margin: 0; }
/* Shown only once there is more than one army to order. The armies view has no
   faction, so --acc is undefined here and the active chip would paint white on
   white, the picker had exactly that bug. It takes the navy instead. */
.dzc-list-sort { margin: -4px 0 14px; }
.dzc-list-sort .dzc-chip.is-active {
  background: var(--navy, #1b3a5c); border-color: var(--navy, #1b3a5c); color: #fff;
}
.dzc-army-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(270px, 100%), 1fr)); gap: 8px; }
.dzc-army-card { padding: 8px 9px; cursor: pointer; border: 1px solid var(--line, #d8d0c0); border-left: 4px solid var(--acc); border-radius: 8px; background: var(--surface-2, #fff); }
.dzc-army-card:hover { border-color: var(--acc); }
/* The empty list IS the grid, with one tile in it. Dashed rather than solid so
   it reads as a place for something rather than a thing, and nothing moves on
   hover. A tile that lifts shoves the grid it sits in. */
.dzc-army-new {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; min-height: 148px; cursor: pointer;
  border: 2px dashed var(--line, #d8d0c0); background: transparent;
  color: var(--ink-2, #6b6459);
  font: 600 15px/1.2 var(--font-ui, 'Jost', system-ui, sans-serif);
}
.dzc-army-new:hover { border-color: var(--navy, #1b3a5c); color: var(--ink, #24201c); }
.dzc-army-new:focus-visible { outline: 2px solid var(--navy, #1b3a5c); outline-offset: 2px; }
.dzc-army-card:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
.dzc-army-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
/* The crest and the faction's full name, as one chip. The crest alone was
   unreadable at this size and the short code alone was a shrug; together they
   are what the Dropfleet card does, and it works. */
.dzc-army-fac {
  display: inline-flex; align-items: center; gap: 6px; min-width: 0;
  padding: 3px 8px 3px 4px; background: var(--surface-3, #f2ece0);
}
.dzc-army-fac img { flex: 0 0 auto; width: 20px; height: 20px; object-fit: contain; }
.dzc-army-fac b {
  font: 600 11px/1.2 var(--font-condensed, 'Jost', sans-serif);
  color: var(--ink-2, #5b544a); overflow-wrap: anywhere;
}
.dzc-army-name {
  margin: 7px 0 4px;
  font: 700 18px/1.2 var(--font-display, 'Roboto Slab', serif); overflow-wrap: anywhere;
}
.dzc-army-pts {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 8px;
  margin: 0 0 5px; font-size: 15px;
  font-family: var(--font-numeric, 'Outfit', sans-serif);
  font-variant-numeric: tabular-nums;
}
.dzc-army-pts b { font: 800 26px/1 var(--font-numeric, 'Outfit', sans-serif); color: var(--acc-text, var(--acc)); }
.dzc-army-pts i { font-style: normal; font-size: 12px; color: var(--ink-2, #6b6459); }
.dzc-army-pts span {
  margin-left: auto; font: 400 11.5px/1.3 var(--font-ui, 'Jost', sans-serif);
  color: var(--ink-2, #6b6459);
}
/* How full the list is, at a glance. */
.dzc-army-bar { height: 4px; background: var(--surface-3, #eee5d6); overflow: hidden; margin-bottom: 7px; }
.dzc-army-bar i { display: block; height: 100%; background: var(--acc); }
.dzc-army-bar i.is-near { background: #d98c1f; }
.dzc-army-bar i.is-over { background: var(--danger, #b3261e); }
/* One thumbnail per distinct Unit, edged in its category colour. */
.dzc-army-strip { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
.dzc-army-strip img {
  width: 38px; height: 28px; object-fit: contain;
  border: 1px solid var(--line, #d8d0c0);
  border-left: 3px solid var(--cat); background: var(--surface-3, #f2ece0);
}
.dzc-army-more { align-self: center; font-size: 11px; color: var(--ink-2, #6b6459); }
.dzc-army-time { margin: 0; font-size: 11px; color: var(--ink-2, #6b6459); }
.dzc-army-pts b { font-size: 19px; font-weight: 600; }

/* ═══ Builder ═══ */
.dzc-b-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 6px; }
.dzc-b-name { font: 800 24px/1.2 var(--font-display, 'Roboto Slab', serif); margin: 0 0 4px; overflow-wrap: anywhere; }
.dzc-b-name:focus-visible { outline: 2px solid var(--acc); outline-offset: 3px; }
.dzc-b-sub { margin: 0; font-size: 12.5px; color: var(--ink-2, #6b6459); }
/* The size reads as part of the line it sits in until you go near it. It is
   a fact first and a control second, and dressing it as a button would make
   the faction beside it look broken for not being one. */
.dzc-b-size {
  font: inherit; color: inherit; background: none; border: 0; padding: 0;
  border-bottom: 1px dotted currentColor; cursor: pointer;
}
.dzc-b-size:hover { color: var(--ink, #241f1a); }
/* A popover is a panel, and panels are square (CLAUDE.md §4). */
.game-size-popover { border-radius: 0; }

.dzc-ratios { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
/* border-left is reserved on every row, transparent except is-std's --
   only there does it carry a colour -- so that row's content is not shifted
   3px out of column from the other three's numbers. */
.dzc-ratio { padding: 6px 12px; border-left: 3px solid transparent; border-radius: 6px; background: var(--surface-3, #f2ece0); font-size: 12px; }
/* Standard is the track; the other three are measured against it, so full
   means level with Standard and past full is the breach (3.2). */
.dzc-ratio-track {
  display: block; height: 3px; margin-top: 5px; min-width: 54px;
  background: var(--line, #d8d0c0);
}
.dzc-ratio-track i { display: block; height: 100%; background: var(--cat, var(--acc)); }
.dzc-ratio.is-over .dzc-ratio-track i { background: var(--danger, #b3382c); }
.dzc-ratio span { display: block; font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2, #6b6459); }
.dzc-ratio b {
  font-size: 17px; font-weight: 600;
  font-family: var(--font-numeric, 'Outfit', sans-serif);
  font-variant-numeric: tabular-nums;
}
/* No gap before the slash: "110 /495" is not a ratio, it is two things. */
.dzc-ratio i { font-style: normal; color: var(--ink-2, #6b6459); font-size: 11px; }
.dzc-ratio-n { display: inline-flex; align-items: baseline; min-width: 0; }
/* Standard used to be a solid block of the faction accent with white text --
   the one loud thing on a card of otherwise quiet rows, and worse on a
   red-accent faction (Bioficer's #9c1818), where "this is fine, it's just
   the reference row" read as an alarm. Jet, 2026-08-09: "make the category
   spend less visually intense... idk i don't like that."
   Same chip as Vanguard/Heavy/Support now; a left rule in the category
   colour is the only thing that still marks it as the one the other three
   are measured against, not a value of its own (3.2). */
.dzc-ratio.is-std { border-left-color: var(--cat, var(--acc)); }
.dzc-ratio.is-over { background: var(--err-bg, #fde8e6); color: var(--err-ink, #7a1c15); }

/* ── Create: the one button that should feel like something ──
   Deco corners, a gold sweep that crosses on hover, and brackets that close
   in on the corners. On press it dips and the gold floods, and while the
   faction loads it pulses so the wait reads as work rather than a hang.
   Fluent durations and curves; nothing moves for prefers-reduced-motion. */
.dzc-create {
  position: relative; overflow: hidden; isolation: isolate;
  border-radius: 0;
  letter-spacing: .06em; text-transform: uppercase;
  transition: box-shadow 150ms cubic-bezier(.33,0,.67,1);
}
/* The sweep. A gold band that crosses the face, behind the label. */
.dzc-create::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(105deg, transparent 30%, var(--gold, #c9a92c) 50%, transparent 70%);
  transform: translateX(-120%);
  transition: transform 420ms cubic-bezier(.33,0,.67,1);
}
.dzc-create:hover::before, .dzc-create:focus-visible::before { transform: translateX(120%); }
/* Corner brackets that tighten in on hover. */
.dzc-create::after {
  content: ''; position: absolute; inset: 5px; pointer-events: none;
  border: 1px solid var(--gold, #c9a92c); opacity: 0;
  transition: opacity 200ms cubic-bezier(.33,0,.67,1), inset 200ms cubic-bezier(.33,0,.67,1);
}
.dzc-create:hover::after, .dzc-create:focus-visible::after { opacity: .9; inset: 3px; }
.dzc-create:hover { box-shadow: 0 0 0 2px var(--gold, #c9a92c); }
.dzc-create:active { filter: brightness(.92); box-shadow: none; }
/* Held while the faction loads, so the pause reads as work. */
.dzc-create.is-going { animation: dzc-create-go 900ms cubic-bezier(.33,0,.67,1) infinite; }
@keyframes dzc-create-go {
  0%, 100% { box-shadow: 0 0 0 0 rgba(201,169,44,.55); }
  50%      { box-shadow: 0 0 0 7px rgba(201,169,44,0); }
}
@media (prefers-reduced-motion: reduce) {
  .dzc-create, .dzc-create::before, .dzc-create::after { transition: none; }
  .dzc-create.is-going { animation: none; }
}

/* Capacity beside the name, at a size you can actually read the shapes at. */
.dzc-detail-name { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; }
/* The badge is a fixed 22px box with the svg absolutely filling it and the
   number on top, so all three have to grow together, sizing the svg alone
   did nothing. */
.dzc-detail-cap .dzc-transport { gap: 6px; }
.dzc-detail-cap .dzc-badge { --badge-px: 66px; --cargo-px: 52px; }
.dzc-detail-cap .dzc-sep { font-size: 30px; font-weight: 700; }

/* One block per variant: what it is, the gun that makes it that, the price,
   and the stats it fights with. */
.dzc-variant { padding: 5px 8px; margin-bottom: 6px; border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff); }
/* The count sits on the block, so the head is a row: what the variant is on
   the left, how many of it you have taken on the right. */
.dzc-variant-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-bottom: 6px; font-weight: 600; font-size: 13.5px;
}
.dzc-variant-head > span { min-width: 0; }
/* The rules this Variant alone has (3.2.2), on the Variant rather than on the
   card -- the same move the builder's blocks make, in the reference. */
.dzc-variant-rules { display: flex; flex-wrap: wrap; gap: 3px; margin: 0 0 6px; }
/* A variant you have taken is marked on the block, not only in its number.
   The mix of a Squad should be readable without reading the digits. */
.dzc-v-step { flex: none; }
.dzc-variant:has(.dzc-v-step.is-on) { border-color: var(--acc); }
.dzc-wpn-ma { white-space: nowrap; }

/* ── Commanders in the rail ──
   One card per Commander with a button beneath, instead of a "— none —" select
   on the bottom of every Squad. You take one or two of these; five Squads
   should not mean five empty slots. */
.dzc-cmdr-card { border-left: 3px solid var(--gold, #c9a92c); }
.dzc-cmdr-card.is-loose { border-left-color: var(--warn-ink, #6b4e00); }
.dzc-cmdr-head { display: flex; align-items: center; gap: 8px; }
.dzc-cmdr-head b { display: block; font-size: 13px; }
/* Editable in place, and it has to say so. The same trap as the Group and
   army headings: nobody finds it, or somebody finds it by typing into it. */
.dzc-cmdr-name {
  cursor: text; border-bottom: 1px dotted var(--ink-2, #9a9285);
  overflow-wrap: anywhere;
}
.dzc-cmdr-name:hover { border-bottom-color: var(--acc-text, var(--acc)); }
.dzc-cmdr-name:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
.dzc-cmdr-pts {
  font-family: var(--font-numeric, 'Outfit', sans-serif);
  font-size: 12px; font-weight: 600; color: var(--ink-2, #6b6459);
}
.dzc-cmdr-assign { display: block; margin-top: 7px; font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-2, #6b6459); }
.dzc-cmdr-assign select { width: 100%; margin-top: 3px; padding: 5px 7px; font-size: 12.5px; border-radius: 4px; border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff); color: var(--ink, #24201c); }
.dzc-cmdr-hint { margin: 7px 0 0; font-size: 12px; color: var(--warn-ink, #6b4e00); }
.dzc-cmdr-remove {
  display: inline-flex; align-items: center; gap: 4px; margin-top: 8px;
  padding: 3px 8px; border-radius: 4px; cursor: pointer;
  border: 1px solid var(--line, #d8d0c0); background: transparent;
  font-size: 11.5px; color: var(--ink-2, #6b6459);
}
.dzc-cmdr-remove:hover { border-color: #a3231a; color: #a3231a; }
/* The empty slot, sized like the Dropfleet Add Admiral panel Jet showed:
   a real dashed panel you can hit, not a thin strip. */
.dzc-cmdr-add {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; padding: 18px 12px; cursor: pointer;
  border: 1px dashed var(--line, #cdc4b2);
  background: transparent; color: var(--ink-2, #5b544a);
  font: 600 14px/1 var(--font-ui, 'Jost', sans-serif);
  letter-spacing: .02em;
  transition: border-color 150ms cubic-bezier(.33,0,.67,1),
              color 150ms cubic-bezier(.33,0,.67,1),
              background 150ms cubic-bezier(.33,0,.67,1);
}
.dzc-cmdr-add .dzc-i { opacity: .8; }
.dzc-cmdr-add:hover {
  border-color: var(--gold, #c9a92c); color: var(--ink, #24201c);
  background: rgba(201,169,44,.07);
}
.dzc-cmdr-add:hover .dzc-i { opacity: 1; }

/* Choose a Commander */
.dzc-cmdr-opt {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px; margin-bottom: 6px;
  border: 1px solid var(--line, #d8d0c0); border-radius: 8px; background: var(--surface-2, #fff);
}
.dzc-cmdr-opt-body { flex: 1 1 auto; min-width: 0; }
.dzc-cmdr-opt-body b { display: block; font-size: 14px; }
.dzc-cmdr-opt-body p { margin: 1px 0 0; font-size: 12px; color: var(--ink-2, #6b6459); }
.dzc-cmdr-opt-pts {
  font-family: var(--font-numeric, 'Outfit', sans-serif);
  font-size: 16px; font-weight: 600; color: var(--acc-ink, #1b3a5c);
}
/* What the Level buys, in the same value-over-word shape the stat grid uses,
   so three numbers read as one block rather than three loose chips. */
.dzc-cmdr-buys { display: flex; gap: 12px; }
.dzc-cmdr-buys span { display: flex; flex-direction: column; align-items: center; line-height: 1.1; }
.dzc-cmdr-buys b {
  font-family: var(--font-numeric, 'Outfit', sans-serif);
  font-size: 14px; font-weight: 600; color: var(--ink, #24201c);
}
.dzc-cmdr-buys i {
  font-style: normal; font-size: 9.5px; text-transform: uppercase;
  letter-spacing: .05em; color: var(--ink-2, #6b6459);
}
/* The same trio in the rail, where it is about the army rather than about one
   option. It carries .dzc-rail-card as well, so it is inset and bordered like
   everything else in that column.

   It was written without a card at first, on the argument that it belongs to
   the Commanders above it and a border would say otherwise. Looked at, that
   was wrong: with no surface it sat flush to the rail's edge while every card
   beside it was inset, and read as three loose numbers dropped between a card
   and a button. */
.dzc-cmdr-buys--rail { justify-content: space-between; }
.dzc-cmdr-buys--rail b { font-size: 16px; }

/* ── builder: rail + column ──
   Desktop keeps panes, mobile does not (HANDOFF §2.2). The rail holds what you
   need while working, points left and what is outstanding, so it stays put
   rather than shoving the Groups down the page each time an alert appears. */
/* Three panes, each scrolling on its own. Dropfleet's builder-layout
   (app.css:507). The overview stays scannable while the detail pane carries
   the weapon tables; one long column made you scroll past a full stat block to
   reach the next Group. */
.dzc-b-body {
  display: grid; grid-template-columns: 250px minmax(200px, 300px) minmax(0, 1fr);
  gap: 8px; align-items: start;
}
.dzc-b-list { display: flex; flex-direction: column; gap: 6px; position: sticky; top: 12px; }
.dzc-b-detail { min-width: 0; }
.dzc-b-none { margin: 0; padding: 14px 2px; font-size: 13px; color: var(--ink-2, #9a9184); }

/* An overview entry: what you need to choose between Groups, nothing to read.
 *
 * Used to be one <button> for the whole card. Baxter, 2026-08-09: "Cannot
 * duplicate or delete from the full list of groups" -- those two actions only
 * ever lived in the drilled-in Group header, so a button nested inside this
 * button was the only place to put them, and a button cannot contain another
 * one. The card is a plain div now; .dzc-bb-select is the button that opens
 * the Group, .dzc-bb-actions the two that duplicate or remove it without
 * opening anything. */
.dzc-bb {
  display: flex; flex-direction: column; gap: 4px; width: 100%;
  padding: 8px 9px; text-align: left;
  border: 1px solid var(--line, #d8d0c0); border-left: 3px solid transparent;
  border-radius: 0; background: var(--surface-2, #fff); color: inherit;
  transition: border-color .12s, background .12s;
}
.dzc-bb:hover { border-color: var(--acc); }
.dzc-bb.is-on { border-left-color: var(--acc-text, var(--acc)); background: var(--surface-3, #f2ece0); }
.dzc-bb.is-over { border-color: var(--danger, #b3261e); }
.dzc-bb-select {
  display: flex; flex-direction: column; gap: 4px; width: 100%;
  padding: 0; border: 0; background: none; color: inherit; font: inherit;
  text-align: left; cursor: pointer;
}
.dzc-bb-select:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
/* Bottom-right, same as the drilled header's own pair -- one glance says
   these two act on the whole Group, not on whatever Squad the card happens
   to be showing art for. */
.dzc-bb-actions { display: flex; justify-content: flex-end; gap: 2px; margin-top: -2px; }
/* This row used to reserve 24px on the right for a grip in the corner, so
   every Group read "100/50" with the last digit crowded against the dots.
   And a Group at its ceiling is exactly the number you are checking. The grip
   moved to the leading edge; the row gets its width back. */
.dzc-bb-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.dzc-bb-head b { font: 700 13.5px/1.2 var(--font-ui, 'Jost', sans-serif); overflow-wrap: anywhere; }
.dzc-bb-head i {
  font-style: normal; white-space: nowrap;
  font: 700 13px/1 var(--font-numeric, 'Outfit', sans-serif); color: var(--acc-text, var(--acc));
}
.dzc-bb-head i s { text-decoration: none; font-weight: 500; font-size: 11px; color: var(--ink-2, #9a9184); }
.dzc-bb.is-over .dzc-bb-head i { color: var(--danger, #b3261e); }
.dzc-bb-meta { font-size: 11px; color: var(--ink-2, #6b6459); }
/* What carries this Group, named. Wraps rather than truncating -- a Group with
   three kinds of Transport gets three lines (CLAUDE.md §4). */
.dzc-bb-carriers {
  display: flex; flex-wrap: wrap; gap: 2px 10px;
  font-size: 11px; color: var(--acc-text, var(--acc, #1b3a5c));
}
.dzc-bb-carriers span { display: inline-flex; align-items: center; gap: 4px; }
.dzc-bb-carriers svg { flex: 0 0 auto; opacity: .8; }
.dzc-bb-spaces { display: flex; flex-wrap: wrap; gap: 3px 8px; }
.dzc-bb-sp { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; }
.dzc-bb-sp b { font: 700 12px/1 var(--font-numeric, 'Outfit', sans-serif); }
.dzc-bb-sp i { font-style: normal; color: var(--ink-2, #9a9184); }
.dzc-bb-sp.is-full b { color: #14532d; }
.dzc-bb-sp.is-over b { color: var(--danger, #b3261e); }
.dzc-bb-art { display: flex; gap: 3px; flex-wrap: wrap; }
.dzc-bb-art img { width: 30px; height: 22px; object-fit: contain; background: var(--surface-3, #f2ece0); }
.dzc-rail { position: sticky; top: 12px; display: flex; flex-direction: column; gap: 8px; }
.dzc-rail-card {
  padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff);
}
.dzc-rail-title {
  margin-bottom: 6px;
  font: 700 10.5px/1.4 var(--font-condensed, 'Jost', sans-serif);
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2, #6b6459);
}
.dzc-rail-pts { display: flex; align-items: baseline; gap: 5px; margin-top: 6px; }
.dzc-rail-pts b {
  font-family: var(--font-numeric, 'Outfit', sans-serif);
  font-size: 30px; font-weight: 600; line-height: 1;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--acc-ink, #1b3a5c);
}
.dzc-rail-pts span { font-size: 12px; color: var(--ink-2, #6b6459); }
.dzc-rail-pts.is-over b { color: #a3231a; }
.dzc-rail-track {
  height: 4px; margin: 7px 0 6px; border-radius: 2px;
  background: var(--surface-3, #f2ece0); overflow: hidden;
}
.dzc-rail-track i { display: block; height: 100%; background: var(--acc); }
.dzc-rail-pts.is-over ~ .dzc-rail-track i { background: #a3231a; }
.dzc-rail-line {
  margin: 1px 0; font-size: 11.5px; color: var(--ink-2, #6b6459);
  font-variant-numeric: tabular-nums;
}
/* The army's model count, beside its Group count. Divided by a rule rather
   than a comma: the two are separate facts about the same army, and a comma
   would read as one sentence about Groups. */
.dzc-rail-models::before {
  content: ''; display: inline-block; width: 1px; height: .85em;
  margin: 0 7px -1px; background: currentColor; opacity: .4;
}
.dzc-rail .dzc-ratios { flex-direction: column; gap: 4px; margin: 0; }
/* Grid, not flex-with-space-between: space-between only spaces out ONE row's
   own items, so "Standard" (one word) and "Vanguard" (also one word, but
   sat beside "0 of 60" where Standard sits beside just "60") pushed their
   numbers to different x positions every row. Fixed columns line the four
   numbers up under each other instead, Jet, 2026-08-09: "align numbers in
   category spend." */
/* The name column is 68, not 60. At 60 "Standard" wanted 62 and "Vanguard" 64
   and both ran out of their track -- the four numbers stayed lined up, which
   is what the column is for, and the words they belong to did not fit. It only
   shows up when the webfont has not loaded, which is the offline case this app
   is built for, so it is measured against the fallback face rather than Jost.
   The track beside it is 83px against a 54px minimum, so the 8px comes out of
   slack. */
/* THE NUMBER COLUMN HOLDS THE WHOLE NUMBER.
   It was 50px against "110 of 495", which is about 68, and .dzc-ratio-n is
   nowrap -- so the ratio ran straight out of its cell and printed over the
   meter in the column beside it. Reported by Jet, 2026-08-17, with a
   screenshot of "80 of 495" lying across its own track.
   Two fixes, because either alone leaves it tight: the word became a slash
   (see the ratio markup), and the column is 74px. Measured rather than
   guessed: "110/495" comes to 61px and a 3000pt Behemoth army's "1495/1500"
   to about 70, so 62 would have shipped the same bug one army size up. The
   name column stays 68 -- that width was measured against the fallback face
   for a reason, and taking it back would re-break "Standard" and "Vanguard".
   The room comes out of the gaps and the meter, which still clears its 54px
   minimum in a 250px rail. */
.dzc-rail .dzc-ratio {
  display: grid; grid-template-columns: 68px 74px minmax(54px, 1fr);
  align-items: baseline; column-gap: 6px; padding: 4px 8px;
}
.dzc-rail .dzc-ratio span { display: inline; font-size: 10.5px; }
.dzc-rail .dzc-ratio-n { display: flex; align-items: baseline; white-space: nowrap; }

/* Three panes need real width. The weapon table alone wants ~620px, so below
   1400 the overview folds up into a row and the detail pane takes the width
   under it, two columns, not a squeezed three.

   Both fold into COLUMN 2, beside the rail. They used to be `grid-column:
   1 / -1`, which put each on a row of its own spanning the whole width and
   left the rail alone in row 1 with a void where the other two columns were.
   250px of card and a thousand pixels of nothing beside it, then the list, then
   the detail, all stacked. That is not "two panes"; it is one pane and a hole,
   and it was the layout on every window from 901px to 1400px. An ordinary
   1280 laptop included, not just a window dragged narrow.

   The rail stays sticky in column 1 where it is useful, and the list sits on
   top of the detail in column 2. */
@media (max-width: 1400px) {
  .dzc-b-body { grid-template-columns: 250px minmax(0, 1fr); }
  .dzc-b-list {
    grid-column: 2; position: static;
    flex-direction: row; flex-wrap: wrap; align-items: stretch;
  }
  .dzc-bb { width: auto; flex: 1 1 190px; }
  .dzc-b-detail { grid-column: 2; }
}

/* Desktop never needs it: both panes are on screen at once. */
.dzc-b-back { display: none; }

@media (max-width: 900px) {
  .dzc-b-body { grid-template-columns: 1fr; }
  /* One column means column 1. The 1400px rule above parks both of these in
     column 2, and dropping to a single track does not undo it. Grid invents
     an implicit second column instead, so the phone got a dead empty gutter on
     the left and the whole army pushed off it. */
  .dzc-b-list, .dzc-b-detail { grid-column: 1; }
  /* The list IS the screen until you pick a Group, and then the Group is. */
  .dzc-b-body .dzc-b-detail { display: none; }
  .dzc-b-body.is-drilled .dzc-b-list { display: none; }
  .dzc-b-body.is-drilled .dzc-b-detail { display: block; }
  .dzc-b-back {
    display: inline-flex; align-items: center; gap: 6px;
    margin-bottom: 8px; padding: 8px 12px; cursor: pointer;
    border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff);
    color: inherit; font: 600 13px/1 var(--font-ui, 'Jost', sans-serif);
  }
  .dzc-rail { position: static; }
  .dzc-rail .dzc-ratios { flex-direction: row; flex-wrap: wrap; }
  /* The peek line, phone only. Above 900px the rail is a column beside the
     list and there is nothing to collapse.
     Two classes deep on purpose: the default that hides it is a single class
     LATER in the file, so a single-class rule here lost at every width and the
     phone got no rail at all. No points left, no Group count, no issue count,
     the three numbers the peek exists to keep on screen. */
  .dzc-rail .dzc-rail-peek { display: flex; }
  .dzc-rail-body { display: none; }
  .dzc-rail-body.is-open { display: block; }
  .dzc-rail-body > * + * { margin-top: 8px; }
}

/* Hidden by default so the desktop rail is untouched: the phone rule above
   turns it on. */
.dzc-rail-peek {
  display: none;
  align-items: baseline; gap: 7px; width: 100%; padding: 9px 11px; cursor: pointer;
  border: 1px solid var(--line, #d8d0c0); border-left: 3px solid var(--acc);
  background: var(--surface-2, #fff); color: var(--ink, #24201c); text-align: left;
}
.dzc-rail-peek b { font: 700 18px/1 var(--font-numeric, 'Outfit', sans-serif); }
.dzc-rail-peek span { font-size: 11.5px; color: var(--ink-2, #6b6459); }
.dzc-rail-peek i { flex: 1 1 auto; font-style: normal; font-size: 11.5px; color: var(--ink-2, #6b6459); }
.dzc-rail-peek em {
  font-style: normal; font-size: 11.5px; font-weight: 600; color: var(--ink-2, #6b6459);
}
.dzc-rail-peek em.is-err { color: var(--danger, #b3261e); }
.dzc-rail-peek em.is-ok { color: var(--acc-text, var(--acc)); }
.dzc-rail-peek svg { flex: 0 0 auto; align-self: center; color: var(--ink-2, #6b6459); }

.dzc-issues { margin: 0 0 6px; padding: 6px 8px; font-size: 13px; }
.dzc-issues-head {
  margin: 0 0 3px;
  font: 700 10.5px/1.4 var(--font-condensed, 'Jost', sans-serif);
  text-transform: uppercase; letter-spacing: .05em; opacity: .8;
}
.dzc-issues ul { list-style: none; margin: 0; padding: 0; }
.dzc-issues li { padding: 2px 0; }
.dzc-issues--err { background: var(--err-bg, #fde8e6); color: var(--err-ink, #7a1c15); }
.dzc-issues--warn { background: var(--warn-bg, #fdf3d8); color: var(--warn-ink, #6b4e00); }
/* The rule reference belongs at the end of the sentence, quiet, not worn as a
   badge on the front where it reads as a label for the whole line. */
.dzc-rulecite { opacity: .6; font-size: 11.5px; white-space: nowrap; }
/* How many Squads hit the same rule, in front of the sentence -- because the
   sentence names the model, not the Squad, so the second copy of it was the
   same words twice. */
.dzc-issue-n { font-family: var(--font-numeric, 'Outfit', sans-serif); font-weight: 700; }
.dzc-legal {
  display: flex; align-items: center; gap: 5px;
  margin: 0 0 8px; font-size: 13px; font-weight: 600;
  color: var(--success-ink, #2f7a37);
}

.dzc-group-card { border: 1px solid var(--line, #d8d0c0); padding: 5px 7px; margin-bottom: 5px; background: var(--surface-2, #fff); }
.dzc-group-card.is-over { border-color: #b3261e; }
.dzc-g-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 10px; margin-bottom: 5px; }
.dzc-g-head h2 { margin: 0; font: 700 15px/1.2 var(--font-ui, 'Jost', sans-serif); overflow-wrap: anywhere; }
/* The two buttons stay at the end of the line whatever the name does. */
.dzc-g-head .dzc-icon-btn { margin-left: 0; }
.dzc-g-head .dzc-icon-btn:first-of-type { margin-left: auto; }
.dzc-g-head h2:focus-visible { outline: 2px solid var(--acc); outline-offset: 3px; }

/* ON A PHONE THE BUTTONS SHARE THE TITLE'S LINE.
 *
 * Jet, 2026-08-12, on the Group header: "these buttons are weird. Too much big
 * wide dead space." Measured at 390: the name and the three meters came to
 * 307px of 344, leaving 37px for two 44px squares, so the pair wrapped -- and
 * `margin-left: auto` then threw them at the far right of a line of their own.
 * A 44px band with 246px of nothing to the left of two icons.
 *
 * Self-inflicted, and recently: these were 24px until the touch-target sweep
 * took every icon button to 44 (mobile-fixes.css), which is the size they
 * should be and is also 40px more than the line had spare. The fix is which
 * thing yields, not how big the target is.
 *
 * So the METERS take the second line and the buttons stay up with the name
 * they act on -- title left, actions right, which is where the trash can
 * already sits on every Squad row beneath this. Order does it rather than a
 * grid, because the header must still collapse to one line on desktop, where
 * there is room for all of it and nothing to fix. */
@media (max-width: 768px) {
  .dzc-g-head { align-items: center; }
  .dzc-g-head h2 { order: 1; flex: 1 1 auto; }
  .dzc-g-head .dzc-icon-btn { order: 2; }
  .dzc-g-head .dzc-g-meters { order: 3; flex: 1 0 100%; }
}
.dzc-g-empty { margin: 4px 0 8px; font-size: 12px; color: var(--ink-2, #6b6459); }

/* What the Group costs, how big it is, and what its Transports still have room
 * for. Icon, number, then the unit spelled out -- "3" beside a shape is not an
 * answer to anything on its own. Wraps rather than clipping. */
/* Three small facts, as plain text. Boxing each one turned the top of every
   Group into a row of grey pills competing with the Squads beneath it. */
/* On the Group's title line, quiet. They were a band of their own under the
   heading with the numbers set at 15px bold, which made three facts you glance
   at louder than the Group they belong to. Same line, 11px, no bold, and the
   ink is the page's own black rather than a grey -- small and unemphasised is
   not the same as faint. */
.dzc-g-meters {
  display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; margin: 0;
}
.dzc-meter {
  display: inline-flex; align-items: baseline; gap: 4px;
  font-size: 11px; color: var(--ink, #2c2a26);
}
.dzc-meter svg { flex: 0 0 auto; align-self: center; opacity: .45; }
.dzc-meter b { font: 400 11px/1 var(--font-numeric, 'Outfit', sans-serif); color: var(--ink, #2c2a26); }
.dzc-meter i { font-style: normal; color: var(--ink-2, #6b6459); }
.dzc-meter.is-over, .dzc-meter.is-over b { color: var(--danger, #b3261e); }

/* Capacity, at size and out of a box. This is the fact that decides whether
   the next thing you pick can join the Group at all (3.2.4.2) -- it is not one
   more small grey pill in a row of small grey pills. */
.dzc-g-space {
  display: flex; flex-wrap: wrap; gap: 10px 24px; align-items: center;
  margin: 0 0 6px; padding: 5px 0;
}
.dzc-space { display: inline-flex; align-items: center; gap: 9px; }
.dzc-space svg { flex: 0 0 auto; }
.dzc-space-n { display: inline-flex; align-items: baseline; font-family: var(--font-numeric, 'Outfit', sans-serif); }
.dzc-space-n b { font-size: 26px; font-weight: 700; line-height: 1; color: var(--ink, #2c2a26); }
.dzc-space-n s { text-decoration: none; font-size: 18px; color: var(--ink-2, #6b6459); padding: 0 2px; }
.dzc-space-n em { font-style: normal; font-size: 18px; font-weight: 600; color: var(--ink-2, #6b6459); }
.dzc-space.is-full .dzc-space-n b { color: #14532d; }
.dzc-space.is-over .dzc-space-n b { color: var(--danger, #b3261e); }

/* Nesting is drawn: a carried Squad is indented under its carrier. The indent
   is a variable because 22 levels of it would eat a phone alive; it drops to 8
   at the narrow breakpoint rather than the tree being flattened away. */
.dzc-squad {
  margin-left: calc(var(--depth) * var(--sq-indent, 22px));
  padding: 4px 0; border-top: 1px solid var(--line, #eee5d6);
}
.dzc-squad:first-of-type { border-top: 0; padding-top: 4px; }

/* WHAT RIDES IN WHAT: ONE RAIL, OUT OF THE MODEL, DOWN THE SIDE OF ITS CARGO.

   Jet's sketch was right and three attempts at it were wrong. This is the
   fourth, and it is the sketch with the geometry measured instead of guessed.

   WHY THE LAST ONE DID NOT WORK, in numbers off the rendered card:

     the spine sat at x=40; the carrier's picture spans x=50-118
       -- 10px out, in the drag-handle gutter, missing the model entirely
     the carrier's picture ends at y=143; the line began at y=365
       -- a 222px gap, with the whole stat table and a weapon card in it

   So what was drawn was a 20px elbow floating in a margin, starting nowhere
   and pointing at nothing. Jet, looking at it: "it feels to me like the lines
   still don't go."

   Two fixes, and the second is the one that matters:

   1. --mini-x is 37px, not 27px. Measured against the rendered card: the
      thumbnail's left edge is 37px into the row, not 27. The old number was
      right for a row that no longer exists.

   2. THE RAIL IS ONE STROKE AND IT BELONGS TO THE CARRIER, not to the cargo
      block underneath it. It starts at the BOTTOM OF THE CARRIER'S PICTURE
      and runs to the bottom of the last thing aboard. That is the only way it
      can leave the model: the cargo block begins after the carrier's stats
      and weapons, so anything anchored there starts 222px too low no matter
      how the arithmetic is arranged. 55px is the constant it needs -- 5px of
      row padding plus the 50px thumbnail, and the strip is 50 tall whether it
      holds one model or twelve.

   THE ARMS ARE GONE, and so is the painted-out tail that existed to hide the
   end of them. An arm is a line that has to AIM, and every fault above was an
   aiming fault. A rail does not aim: everything to the right of it is inside
   the thing above it, which is the whole sentence. It also gives the app one
   line and not two -- the same vertical stroke a Variant block already uses,
   meaning the same thing. */
/* WHERE THE VERTICAL SITS, and this is the sixth attempt's whole subject.
   docs/CARRY-LINE.md is the spec; read it before changing any number here.

   It was at --mini-x, the CARRIER's own picture-left, so it ran down that
   unit's side and read as its left bar -- which Jet has now rejected in those
   words twice. Measured off his drawing: carrier picture left 40, cargo
   picture left 57, vertical at 52. Five pixels left of the CARGO, thirteen
   right of the carrier.

   In this app's numbers that is x = 37 + 52 - 5 = 84: still inside the
   carrier's picture span of 37..142, so it leaves the photograph, but sitting
   in the CARGO's own indent so it reads as something hanging off the carrier
   down to what it holds. Same two endpoints as before, different object.

   --carry-gap is the one number both halves are built from. The drop sits that
   far left of the cargo's picture; the arm crosses exactly that gap and then
   22px more, INTO the picture. Written once so the two can never drift. */
/* BIGGER AGAIN. Jet, 2026-08-13: "can we make pics for variants and main
   units a bit bigger?" 105x75 -> 128x92, a fifth up, and --mini-x moves with
   it because the carry line is measured off the picture and nothing else. */
.dzc-squad { --mini-x: 37px; --rider-step: 52px; --carry-gap: 5px; --mini-w: 128px; --mini-h: 92px; }
/* In the CARRIER row's coordinates. Inside a rider row the same stroke is
   --rider-step further left, which is `--mini-x - --carry-gap`. */
.dzc-squad { --carry-x: calc(var(--mini-x) + var(--rider-step) - var(--carry-gap)); }
.dzc-riders { position: relative; margin: 4px 0 0; padding-left: var(--rider-step); }
.dzc-riders > .dzc-squad {
  position: relative; margin-left: 0; border-top: 0; padding-top: 6px;
}
/* JET'S SKETCH, AND NOTHING ELSE. Down out of the picture, then RIGHT, INTO
   the picture of the thing it is carrying -- not to the edge of that card, and
   not onward past it. Jet, 2026-08-07, on a Vulture over a Howitzer: "why does
   the line keep going? It should go into the howitzer."

   It kept going because the last version had no arms at all. I had replaced
   them with one long rail down the side of the cargo, on the argument that an
   arm has to aim and aiming kept missing. That was me substituting my idea for
   the one I was given: the rail runs past the thing it is meant to point at
   and carries on to the bottom of the card, which is exactly what Jet is
   looking at.

   The arms are back. What was actually wrong all along was the arithmetic, and
   it is now measured rather than guessed:

     --carry-x  84px, five left of the CARGO's picture and inside the span of
                the carrier's. It was --mini-x, 37 -- the carrier's own picture
                left, which is what made it read as that unit's left bar. See
                the block above; that move is the whole of the sixth attempt.
     top 80px   5px of row padding + the 75px picture box = the bottom of the
                photograph. The old drop started at the top of the cargo block
                instead, 222px below the picture, with the carrier's whole stat
                card in between.
     top 44px   6px of rider row padding + half the 75px picture = the middle
                of the picture the arm points at.
     +22px      how far INTO that picture the arm finishes. Jet: "into the
                model, not to the edge of that card."

   Three shapes, and the third is a lie told carefully: the drop runs to the
   bottom of the carrier's row because CSS cannot know where the last arm is,
   so the last rider paints the tail below its own arm out in the card's own
   colour. --surface-2, so it stays painted out on a dark page, and it sits in
   the empty channel left of every rider's content, never over anything.

   Hung off .is-carrier, which squadHtml emits, rather than off :has() -- a
   line that draws only where a selector is supported is a line that can
   silently not exist, and no test here can see that. */
.dzc-squad.is-carrier { position: relative; }
/* The drop, out of the bottom of the carrier's photograph. */
.dzc-squad.is-carrier::after {
  content: ''; position: absolute; left: var(--carry-x);
  /* 5px of row padding + the picture box = the bottom of the photograph. */
  top: calc(5px + var(--mini-h, 75px)); bottom: 0; width: 3px;
  background: var(--acc, #1b3a5c);
}
/* A rider row carries 2px more padding than a top-level one, so a carrier that
   is itself cargo leaves its picture one pixel lower. */
.dzc-riders > .dzc-squad.is-carrier::after { top: calc(6px + var(--mini-h, 75px)); }
/* The arm, into the middle of the picture of what is aboard.

   ON .dzc-sq-main, not on the row, and that is the whole of the last bug. A
   Squad in the middle of a chain -- a Buggy inside a Raven with Legionnaires
   inside IT -- plays three parts at once: it is cargo (it needs an arm), it is
   a carrier (it needs a drop), and it is the last thing in its block (it needs
   the paint-out). Three jobs, two pseudo-elements. The drop and the paint-out
   were both written to ::after, the paint-out rule came later in the file and
   won, and the Buggy's drop simply never drew -- which is why the Legionnaires
   ended up with an arm hanging in mid-air, pointing into their picture from
   nothing.

   The arm moves down onto the row's own header, where it is measured against
   the picture it points at rather than against the row: 5px of header padding
   plus half the 75px picture. That frees ::after for the drop and ::before for
   the paint-out, and the three stop fighting. */
.dzc-riders > .dzc-squad > .dzc-sq-main { position: relative; }
.dzc-riders > .dzc-squad > .dzc-sq-main::after {
  content: ''; position: absolute;
  /* The parent's drop, in this row's coordinates. */
  left: calc(var(--mini-x) - var(--carry-gap));
  /* Half the picture, less half the 3px bar, and measured against
     .dzc-sq-main whose top IS the picture's top. */
  top: calc(var(--mini-h, 75px) / 2 - 1.5px); height: 3px;
  /* A SHORT TICK, not a traverse. The drop now stands 5px off this picture's
     left edge, so the arm crosses that gap and finishes 22px inside the
     photograph -- 27px in total, where it used to be 74. Jet's drawing shows
     the two pictures very nearly sharing a column, and this is the reason:
     move the vertical into the cargo's indent and the horizontal has almost
     nothing left to cross. */
  /* AND IT STOPS AT THE BOX. Jet, 2026-08-13: "can we have the lines go to
     the edge of the box behind the unit rather than the inside?" The picture
     sits on a filled panel, so the last 22px of this arm were drawn ACROSS
     the photograph -- a line running over the model it points at. It reaches
     the panel's left edge and ends there. --carry-gap alone is that distance,
     which is the same number the drop is placed with, so the two cannot
     drift apart. */
  width: var(--carry-gap);
  background: var(--acc, #1b3a5c);
}
/* AND THEY DRAW ON. Jet, 2026-08-13: "when the lines are added, could they be
   drawn on?" -- the same ask as the variant line, which has done this since
   2026-08-07, and the carry line is the one stroke in a Group card he has
   said is worth keeping.

   The drop grows downward out of the carrier's photograph and the arm grows
   sideways into the cargo's, both on transform alone so nothing reflows and a
   phone can hand the whole thing to the compositor. scaleY/scaleX rather than
   height/width for the same reason: animating a length on a positioned
   pseudo-element relays out every Squad under it, every frame. */
@keyframes dzc-carry-drop { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes dzc-carry-arm  { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.dzc-squad.is-linked.is-carrier::after {
  transform-origin: top center;
  animation: dzc-carry-drop .34s var(--ease-out, cubic-bezier(0, 0, 0, 1)) 1;
}
.dzc-riders > .dzc-squad.is-linked > .dzc-sq-main::after {
  transform-origin: left center;
  /* After the drop it hangs off, so the line reads as one stroke turning a
     corner rather than two marks appearing at once. */
  animation: dzc-carry-arm .2s var(--ease-out, cubic-bezier(0, 0, 0, 1)) .28s backwards;
}
@media (prefers-reduced-motion: reduce) {
  .dzc-squad.is-linked.is-carrier::after,
  .dzc-riders > .dzc-squad.is-linked > .dzc-sq-main::after { animation: none; }
}
/* And the PARENT's drop stops at the last thing it reaches.

   At the parent's x, which is 52px left of this row's own drop -- so a Squad
   that is both cargo and a carrier paints this out without touching the line
   it is itself sending down to what IT carries. */
.dzc-riders > .dzc-squad:last-child::before {
  content: ''; position: absolute;
  left: calc(var(--mini-x) - var(--carry-gap));
  /* 6px of rider-row padding + the arm's top + its 3 = just under the arm.
     Derived, so it follows the picture down when the box shrinks -- left as a
     literal it painted out from 45px on a phone whose arm was at 26. */
  top: calc(6px + var(--mini-h, 75px) / 2 - 1.5px + 3px); bottom: -4px; width: 3px;
  background: var(--surface-2, #fff);
  /* ABOVE the drop, and it has to be said out loud. ::after is generated as
     the carrier's LAST child, so the drop is later in tree order than the
     riders inside it -- and two positioned boxes at z-index auto paint in tree
     order. The paint-out was landing UNDERNEATH the line it exists to end, and
     the line ran on to the bottom of the card exactly as before. Every number
     was right; the paint order was not. */
  z-index: 1;
}
/* AND SO DOES EVERY RIDER'S, which is the bug the first picture of this
   geometry caught.

   The parent's stroke lands at THIS row's x=32. It used to land at -15, out in
   the drag-handle gutter belonging to the row above, which is why the comment
   on the paint-out could say it "sits in the empty channel left of every
   rider's content, never over anything". That is no longer true of either
   line: the parent's drop runs through a non-last rider's stat table, and on
   the last one the paint-out -- a 3px stripe of --surface-2, white -- draws a
   scratch straight through the cargo's stats and its rule chips. In the OG
   shot it went through "Command Centre" and all three stat rows.

   Same fix as the carrier's own, one stroke further left: 32 + 3 + 9 of
   clearance. The .is-carrier rule below must stay AFTER this one -- equal
   specificity, later wins -- because a rider that is itself a carrier has to
   clear its OWN drop at 84 as well, and 96 covers both. */
.dzc-riders > .dzc-squad > .dzc-sq-body,
.dzc-riders > .dzc-squad > .dzc-vblock,
.dzc-riders > .dzc-squad > .dzc-variants,
.dzc-riders > .dzc-squad > .dzc-rm {
  padding-left: calc(var(--mini-x) - var(--carry-gap) + 12px);
}
/* AND THE CARRIER'S OWN CONTENT CLEARS IT. The drop falls through the band
   the carrier's stat table and weapon cards occupy, so they start to the right
   of it -- 9px clear of the 3px stroke, the same clearance this rule has
   always given, just measured off the new x. Indented, never clipped. */
.dzc-squad.is-carrier > .dzc-sq-body,
.dzc-squad.is-carrier > .dzc-vblock,
.dzc-squad.is-carrier > .dzc-variants,
.dzc-squad.is-carrier > .dzc-rm {
  padding-left: calc(var(--carry-x) + 12px);
}
.dzc-squad > .dzc-sq-body, .dzc-squad > .dzc-vblock,
.dzc-squad > .dzc-variants, .dzc-squad > .dzc-rm { padding-left: 12px; }

/* RAW MATERIALS, and a Genitor is ALWAYS a carrier -- the hollow square that
   makes it one is the same symbol that sets the RM cap -- so this block is on
   the carry line's path every single time it draws. Both clearance lists
   above name it for that reason. Miss one and the line scratches through it,
   which is the bug the first OG shot of the new geometry caught.

   Square, like every other panel (CLAUDE.md §4). The stepper keeps its own
   radius; a control may be soft, a panel may not. */
.dzc-rm {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 4px; padding-top: 4px;
  border-top: 1px solid var(--line, #eee5d6);
}
.dzc-rm-head {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11.5px; color: var(--ink-2, #6b6459);
}
.dzc-rm-head b { font-weight: 600; color: var(--ink, #2c2a26); }
.dzc-rm-head svg { flex: 0 0 auto; color: var(--acc-text, var(--acc, #1b3a5c)); }
/* The price, beside the name, because that is the whole of how RM is bought:
   one token, 5pts, on this Genitor. It used to say "3 of 12" on the stepper,
   which framed the cap as a ration you were drawing down rather than a limit
   you will not reach in most lists. The cap is still enforced on the + button
   and still says itself when it refuses. */
.dzc-rm-head i {
  font-style: normal; font-size: 11px; color: var(--ink-2, #6b6459);
}
.dzc-rm-pts { margin-left: auto; font-weight: 600; font-size: 13px; }
.dzc-rm-pts small { font-size: 10px; font-weight: 400; color: var(--ink-2, #6b6459); }
/* A Squad reads as the unit does when you open it: art, the capacity symbol at
   size beside the name, the meta line, the stats, the rules, a block per
   variant and the whole weapon table. Laid out with flex and wrapping rather
   than a fixed grid, so the same markup collapses down a phone without a
   second build. */
.dzc-sq-main { display: flex; align-items: flex-start; gap: 9px; flex-wrap: wrap; }
/* ONE PICTURE PER SQUAD, IN A BOX THAT NEVER CHANGES SIZE.

   It drew one thumbnail per model in a ceil(sqrt(n)) grid -- a Squad of nine
   was nine copies of the same photograph at a third of the size. Jet,
   2026-08-07: "I no longer wish for like, adding more units to actually
   increase the # of minis visible. That's a bit silly." It said nothing the
   count beside it did not say better, and it shrank the one thing you are
   meant to recognise the more of them you took.

   The fixed 84x60 box stays, and it is still what stops a Squad's row changing
   height -- that was the other half of the original problem ("if you add a
   unit, the image addition doesn't fuck things up"), and it is now true by
   construction rather than by arithmetic. */
/* A QUARTER BIGGER. Jet, 2026-08-07: "blow up the pics by like 25%?"
   84x60 -> 105x75. Still one fixed box, so a Squad's row height is still
   independent of how many models are in it -- and the three numbers the
   carry line is built from move with it, below. */
/* THE BOX IS A VARIABLE, and it has to be, because the carry line is measured
   off it: the drop leaves the picture's BOTTOM and the arm lands at its
   MIDDLE. Those were the literals 80 and 36 -- 5px of row padding plus 75, and
   half of 75 -- so shrinking the picture anywhere silently aimed the line at a
   height the photograph no longer had.

   Which is exactly what was happening on every phone. A rule left over from
   the 84x60 days shrank .dzc-sq-art to 68x50 below 860px while .dzc-sq-minis
   stayed 105x75, so the picture sat in the top-left of a box a third bigger
   than itself, the arm pointed at 37px into a 50px-tall image, and 37px of
   dead paper rode along on every Squad on the device this app is for. */
.dzc-sq-minis { flex: 0 0 auto; width: var(--mini-w, 105px); height: var(--mini-h, 75px); }
.dzc-sq-art {
  display: block; width: 100%; height: 100%; object-fit: contain;
  background: var(--surface-3, #f2ece0);
}
.dzc-sq-id { flex: 1 1 220px; min-width: 0; }
.dzc-sq-title { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px; margin: 0; }
/* The capacity symbol, at the size it has in the unit view. It is the fact
   that decides what may join this Group (3.2.4.2), not a footnote. */
.dzc-sq-cap .dzc-transport { gap: 4px; }
.dzc-sq-cap .dzc-badge { --badge-px: 44px; --cargo-px: 35px; }
/* The svg is absolutely positioned inside the badge box and keeps its own
   22px width attribute, so growing the box alone leaves the glyph small.
   The same trap .dzc-detail-cap documents above. */
.dzc-sq-cap .dzc-sep { font-size: 19px; }
/* On the name's line, not under it. Inline-flex so it wraps as one unit when
   the title runs out of room rather than breaking between Category and Type. */
.dzc-sq-meta {
  display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; margin: 0;
  font-size: 11.5px; font-weight: 400; color: var(--ink-2, #6b6459);
}
/* AND IT WRAPS. flex: 1 1 100% below 640px put this on its own line, which
   was the right idea and not enough: a line of its own is 100% of a row that
   is already indented once per level of carry, and this row does not shrink.
   Its children are a Transport button, a stepper, a price and a remove --
   312px of min-content, in a 194px row on the third level of a chain at 320px.
   The basis said 100% and the content said 312, and 312 won by running off the
   side of the page.

   Wrapping is the only honest answer, because nothing here may be hidden or
   clipped: the controls take a second line when there is no room for one. */
.dzc-sq-ctl {
  display: flex; align-items: center; gap: 8px; margin-left: auto;
  flex-wrap: wrap; min-width: 0;
}
/* A control in that row that says what it is.
 *
 * The Transport chooser lived here as a bare 15px lorry beside the bin, and in
 * the 2026-08-09 thread two players never found it -- see the comment on
 * transportPicker for the whole exchange. An icon is a reminder of a control
 * you already know; it is not a name for one you do not. The bin gets away
 * with it because everyone has met that bin. A lorry meaning "choose what
 * carries this" has no such currency.
 *
 * It is not the strip Jet cut. That was a full-width band per Squad announcing
 * "TRANSPORT / Walks on" -- a caption saying nothing had happened. This is a
 * button with its own name on it, which is what a button is meant to have, and
 * it is the same word the panel it opens is titled with.
 *
 * nowrap on the label so "Take out" cannot break in half; .dzc-sq-ctl already
 * wraps the buttons themselves when the row runs out of room, which is the
 * honest way to make two words fit on a phone. */
.dzc-sq-btn {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px;
  min-height: 28px; padding: 3px 9px 3px 7px; white-space: nowrap;
  border: 1px solid var(--line, #d8d0c0); border-radius: 4px;
  background: var(--surface-2, #fff); color: var(--ink-2, #6b6459);
  font: 600 12px/1 var(--font-ui, 'Jost', sans-serif); cursor: pointer;
  transition: border-color .12s, color .12s;
}
.dzc-sq-btn:hover { border-color: var(--acc); color: var(--acc-text, var(--acc)); }
.dzc-sq-btn:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
/* The cost and the bin travel together, so what the row breaks off is a pair
   rather than a lone bin. flex: 1 1 auto and space-between, so on the line it
   wraps to it spreads to the width it has -- the number at one end, the Squad's
   own bin at the other -- and on a line it shares it takes only its content. */
.dzc-sq-tail {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; flex: 1 1 auto; min-width: 0;
}
/* The stat table and the guns, side by side.
   
   .dzc-sq-stats is `width: max-content` -- only as wide as its longest label,
   which is the whole point of it -- so putting the weapon cards underneath
   left a band of empty paper beside five short lines on every Squad in the
   army. The guns take the rest of the row instead.
   
   flex, not a fixed grid: the stat table sizes itself and the weapon grid gets
   what is left, and `min-width: 0` on the guns is what lets their auto-fill
   columns actually shrink instead of pushing the row wide. */
.dzc-sq-body { display: flex; align-items: flex-start; gap: 10px; margin-top: 3px; }
/* As wide as the stats need and no wider -- a Vehicle prints three of them
   and a 300px column beside three short rows is the empty paper this was
   supposed to remove. */
.dzc-sq-body > .dzc-sq-stats { flex: 0 1 auto; margin-top: 0; }
.dzc-sq-body > .dzc-sq-wpn { flex: 1 1 0; min-width: 0; margin-top: 0; }
/* A phone has no room for two of anything. Stats first, then the guns. */
/* AND THE GUNS MUST STILL BE THERE. `flex: 1 1 0` above sets the basis on the
   MAIN axis, and the main axis is the HEIGHT the moment this row turns into a
   column -- so on every screen 700px and under, the weapon block was laid out
   at zero height, and `overflow-x: auto` (which computes overflow-y to auto
   alongside it) clipped every card inside it. Measured on a 390px viewport:
   the block reported height 0 with a scrollHeight of 1274. A Squad's entire
   weapon list, invisible, on the one device this app is used on at a table.

   flex: 0 0 auto -- take the height the cards need, do not absorb free space.
   The width rule above is what makes it full-bleed; the basis has no business
   being 0 once the direction has turned. */
@media (max-width: 700px) {
  .dzc-sq-body { flex-direction: column; gap: 8px; }
  .dzc-sq-body > .dzc-sq-stats, .dzc-sq-body > .dzc-sq-wpn { width: 100%; }
  .dzc-sq-body > .dzc-sq-wpn { flex: 0 0 auto; }
}
.dzc-sq-stats { margin-top: 5px; }
/* The army's name, as the last crumb in the topbar. A chevron separates it
   from the screen behind it, and it is the ONE thing on that bar allowed to
   give way: the trail must stay readable, the name is already on the card you
   came from. */
.dzc-b-name.is-quiet { font-size: 15px; font-weight: 600; margin: 0 0 4px; }
.topbar-crumb {
  display: inline-flex; align-items: center; gap: 8px;
  min-width: 0; font-weight: 600; color: #fff;
}
.topbar-crumb::before {
  content: ""; flex: 0 0 auto; width: 5px; height: 5px;
  border-top: 1.5px solid rgba(255,255,255,.5); border-right: 1.5px solid rgba(255,255,255,.5);
  transform: rotate(45deg);
}
@media (max-width: 560px) { .topbar-crumb { display: none; } }

/* Take / Taken, for a Squad of one model whose Variant is a radio rather than
   a count. Same square language as the Squad-size tabs. */
/* A Variant and its guns, as one block. The head is the name, the price and
   the count; the cards under it are what that Variant fires.

   ONE LINE PER BLOCK, AND IT IS ALWAYS THERE. Jet, 2026-08-07: "the way you
   have the lines? in this pic it's not working. too many lines while also too
   few. mostly too many. also too hard to use."

   Too many, and he was right: a UCM Main Battle Tank drew an accent edge on
   the Variant you had taken, an orange edge on every variant-restricted
   weapon card inside it, and "Sabre only" printed on each of those cards --
   three marks for one fact, five times over on one Squad. The orange edges and
   the labels are gone (dzc-units.js), and this is what is left.

   Too few, and he was right about that as well: the edge only existed on a
   Variant you had taken, so on a fresh Squad -- every count at zero, which is
   every Squad you have just added -- nothing bounded a block at all. Four
   headings and eleven cards ran together and you worked out where Rapier
   stopped from the whitespace. The line is on every block now: a hairline in
   the rule colour when the Variant is not yours, the faction accent at full
   weight when it is. Present either way, so it is structure; different enough
   that a taken Variant is still findable down a column of four.

   The line is drawn as a child, not a border, because a border cannot be
   animated on from nothing without moving the text beside it. */
.dzc-vblock { position: relative; margin-bottom: 6px; padding-left: 9px; }
/* NO RAIL. Jet, 2026-08-08: "TOO MANY BARS. EXCEPT THIS LINE. this line is
   good." The carry line is the one vertical stroke in a Group card that means
   something, and it was competing with six of these on a single Howitzer --
   same direction, same weight, same accent colour, saying something else
   entirely. A Variant you have taken says so with a tint and its own name in
   the accent; the count beside it says how many. */
.dzc-vblock.is-taken > .dzc-vblock-head {
  background: color-mix(in srgb, var(--acc, #1b3a5c) 9%, transparent);
}
.dzc-vblock.is-taken > .dzc-vblock-head b { color: var(--acc-text, var(--acc, #1b3a5c)); }
/* Bioficer's accent is #9c1818 -- a dark red that reads fine as a hairline or
   a 9% tint but fails as TEXT on the dark theme's own dark surfaces (Baxter,
   2026-08-09: "the red text of the names is a bit hard to read"). --acc is
   set per faction and inline, so there is no single hex to swap in for one
   faction; lightening whatever it is toward white keeps every faction's own
   hue while pulling it clear of the card behind it. Checked against every
   faction accent at 1.4.3's 4.5:1: Bioficer's raw #9c1818 measured 1.76:1 on
   --surface-2 dark, the worst of the seven but not the only one under 4.5 --
   UCM, Scourge, Resistance and Behemoths all failed too. 50% white clears
   every one of them (Bioficer lowest at 5.35:1). */
[data-theme="dark"] .dzc-vblock.is-taken > .dzc-vblock-head b {
  color: color-mix(in srgb, var(--acc, #1b3a5c) 50%, white);
}

/* And it DRAWS ON. Jet, 2026-08-07: "when you click on, the line should like,
   draw on."

   Out of the heading and down past the cards it just claimed, in one stroke.
   Nothing else moves -- the line is out of flow, so the block it belongs to
   does not reflow while it runs. */
@keyframes dzc-vline {
  from { background: color-mix(in srgb, var(--acc, #1b3a5c) 34%, transparent); }
  to   { background: color-mix(in srgb, var(--acc, #1b3a5c) 9%, transparent); }
}
.dzc-vblock.is-drawn > .dzc-vblock-head {
  animation: dzc-vline .32s var(--ease-out, cubic-bezier(0, 0, 0, 1)) 1;
}
@media (prefers-reduced-motion: reduce) {
  .dzc-vblock.is-drawn > .dzc-vblock-head { animation: none; }
}
.dzc-vblock-head {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; margin-bottom: 2px;
}
/* The variant's own miniature, at the size of the line it sits on.
   contain, not cover: these are cut-outs of very different silhouettes, a
   squat tracked tank beside a tall walker, and cropping to fill a box would
   take the legs off the one thing that tells them apart. The box is wider
   than it is tall for the same reason -- most of them are. */
/* 46x30 -> 60x40, on the same ask. It is the only picture that tells a Sabre
   from a Rapier, and it was drawn smaller than the stepper beside it. The
   header is a flex row that centres on its tallest child, so the block grows
   with it rather than the photograph overflowing. */
.dzc-vblock-art {
  flex: 0 0 auto; width: 60px; height: 40px; object-fit: contain;
  object-position: left center;
}
.dzc-vblock-head b { font: 700 13px/1.2 var(--font-ui, 'Jost', system-ui, sans-serif); }
.dzc-vblock-head i {
  font-style: normal; font-size: 11px; color: var(--ink-2, #6b6459);
  font-family: var(--font-numeric, 'Outfit', sans-serif);
}
/* The rules this Variant alone has (3.2.2). Same chips as the Squad's own
   rules, in the block that owns them -- "Scanner (Greave)" is the Greave's,
   and printing it on the Squad said every model in it had a Scanner. */
.dzc-vblock-rules { display: flex; flex-wrap: wrap; gap: 3px; margin: 0 0 3px; }
/* "Nice and big" -- this is the control the whole block exists for. */
.dzc-vblock-n { margin-left: auto; }
/* 40px square was "nice and big" when a Variant block was the only control on
   the row. Six of them set six 40px headers down one Squad, and 32 is still
   well past the 24px anyone can hit. */
.dzc-vblock-n .dzc-stepper button { min-width: 34px; min-height: 32px; }
.dzc-vblock-n .dzc-stepper b { font-size: 16px; min-width: 20px; text-align: center; }

/* Buy this gun. The price IS the button -- the upgrade table that used to
   carry it printed the whole weapon a second time to have somewhere to put a
   price, so it went and the price came here. Big enough to hit with a thumb,
   and it says what it did rather than what it costs once you own it. */
/* LINKED TRANSPORT IS THE LOUD ONE. Jet, 2026-08-13: "let's make the 'add
   linked transport' much more visible, by making it a bit bigger, matching
   the aesthetics of the 'add transports' button."

   Tying a Squad to the thing that carries it is the commonest move in a
   Group, and it was the quietest control on the row: a 12px ghost chip in the
   same grey as Take out. It takes Add Transports' old clothes -- the accent
   fill, the uppercase label, the icon in its own tile -- one size down,
   because this belongs to a Squad and that belongs to the Group. */
.dzc-carry-btn {
  min-height: 36px; padding: 5px 12px 5px 6px; gap: 8px;
  border: 0; border-radius: 0;
  background: var(--acc, #1b3a5c); color: var(--acc-on, #fff);
  font: 700 12px/1 var(--font-ui, 'Jost', sans-serif);
  letter-spacing: .05em; text-transform: uppercase;
  transition: box-shadow 180ms cubic-bezier(.33,0,.67,1), filter .12s;
}
/* The chain rides in its own tile, as the chevron does on Add Units. */
.dzc-carry-btn svg {
  box-sizing: content-box; flex: 0 0 auto; padding: 6px;
  background: rgba(255,255,255,.16);
}
.dzc-carry-btn:hover, .dzc-carry-btn:focus-visible {
  border: 0; color: var(--acc-on, #fff);
  box-shadow: inset 0 0 0 2px var(--gold, #c9a92c);
}
.dzc-carry-btn:active { filter: brightness(.92); box-shadow: none; }
@media (prefers-reduced-motion: reduce) { .dzc-carry-btn { transition: none; } }
/* The rule this Squad is breaking, on this Squad. The rail still counts them;
   this says which one, where you can act on it. */
/* ON the meta line, beside Vehicle -- Jet, 2026-08-07: "put that alert for
   3.2.4 on the unit's rule. like to the right of VEHICLE on that line." A
   block of its own under the card was a second place to look; inline, it is
   read with the Unit it is about. */
.dzc-sq-alert {
  display: inline-flex; align-items: baseline; gap: 4px; margin: 0;
  padding: 1px 6px; font-size: 11px; line-height: 1.35;
  background: rgba(179,38,30,.08); color: var(--ink, #24201c);
  border-left: 3px solid var(--danger, #b3261e);
}
.dzc-sq-alert svg { flex: 0 0 auto; margin-top: 1px; color: var(--danger, #b3261e); }
.dzc-sq-alert i { font-style: normal; }
.dzc-sq-alert:not(.is-err) {
  background: rgba(201,169,44,.12); border-left-color: #b58a12;
}
.dzc-sq-alert:not(.is-err) svg { color: #8a6a10; }

/* Upper right, and NOTHING MOVES when you press it. Jet, 2026-08-07: "the
   +10 points thing shouldn't have anything move... it should be upper-right
   mounted."
   
   It was inline in the card's header, so buying swapped "+10pts" for "Bought",
   the header re-flowed around the new width and the weapon's name shifted
   under the cursor. Out of flow entirely now, pinned to the card's top right,
   with a min-width wide enough for either word so the box itself does not
   change size either. */
.dzc-wc { position: relative; }
.dzc-buy {
  position: absolute; top: 2px; right: 2px; z-index: 1;
  min-width: 62px; min-height: 32px; padding: 3px 8px; cursor: pointer;
  border-radius: 0; text-align: center;
  border: 1px solid #2e9e4f; background: var(--surface-2, #fff); color: #1f7a3a;
  font: 700 11px/1 var(--font-ui, 'Jost', system-ui, sans-serif);
  font-variant-numeric: tabular-nums;
}
/* Room for it, so a long weapon name never runs under the button. */
.dzc-wc-head { padding-right: 66px; }
.dzc-buy:hover { background: rgba(46,158,79,.12); }
.dzc-buy.is-on { background: #2e9e4f; border-color: #2e9e4f; color: #fff; }

/* The one option on a card that takes a gun AWAY instead of selling you one --
   "May remove one UM-117 Cannons and gain Scanner and Scout". Green is the
   colour of a green name box, which is what a paid upgrade is printed in, and
   this is not one. It sits on the row of the gun it drops, so it takes the
   ink's own colour and reads as a subtraction. */
.dzc-buy--drop { border-color: var(--stroke, #d9d0be); color: var(--ink-2, #6b6459); }
.dzc-buy--drop:hover { background: rgba(0,0,0,.06); }
.dzc-buy--drop.is-on {
  background: var(--ink, #24201c); border-color: var(--ink, #24201c); color: var(--surface-2, #fff);
}

/* ── TURNING ON AND OFF ──

   Jet, 2026-08-07: "I want ANIMATIONS FOR THINGS TURNING ON OR OFF across da
   board. shit needs to STOP MOVING AND JUMPING AROUND WHEN I CLICK ON OR OFF."

   Both halves of that are one rule: the change is reported in COLOUR and
   NOTHING ELSE MOVES. No scale, no slide, no lift, no height change -- the
   control stays exactly where it was and the page around it does not reflow,
   because a control that jumps when you press it is a control you have to aim
   at twice.

   Keyframes rather than transitions: the builder redraws the whole army on
   every press, so the element you pressed is replaced by a fresh one and there
   is no "before" for a transition to run from. js/dzc-builder.js flip() is
   what decides which of these two an element gets. */
@keyframes dzc-turn-on {
  0%   { box-shadow: inset 0 0 0 2px currentColor; filter: brightness(1.25); }
  100% { box-shadow: inset 0 0 0 0 transparent; filter: none; }
}
@keyframes dzc-turn-off {
  0%   { box-shadow: inset 0 0 0 2px var(--ink-2, #6b6459); opacity: .55; }
  100% { box-shadow: inset 0 0 0 0 transparent; opacity: 1; }
}
.is-turn-on  { animation: dzc-turn-on .3s var(--ease-out, cubic-bezier(0,0,0,1)) 1; }
.is-turn-off { animation: dzc-turn-off .3s var(--ease-out, cubic-bezier(0,0,0,1)) 1; }
@media (prefers-reduced-motion: reduce) {
  .is-turn-on, .is-turn-off { animation: none; }
}

/* ── Arriving and leaving ──
   Jet, 2026-08-10: "when stuff appears on screen, have animations in. when
   stuff gets deleted, have animations out."

   TRANSFORM AND OPACITY, AND NOTHING ELSE. Not height, not margin, not
   max-height, however much a card collapsing into its own gap would look
   better. Those are layout properties: every frame re-runs layout and paint
   for the whole list underneath, on the main thread, and this app's lists run
   to a dozen Squads each carrying a photograph, a stat grid and a weapon
   table. Transform and opacity are the two the compositor can do on its own
   thread, which is the difference between a phone animating this and a phone
   stuttering through it. It is also why the exit does not close the gap: the
   card fades and shrinks in place, then the redraw takes the space. A gap
   that closes smoothly is not worth a janky one that does.

   The card is offset from the LEADING edge rather than dropped from above.
   A list already reads that way, the grip is over there, and 8px of travel is
   enough to read as "this arrived" without being a slide.

   Durations are Fluent's fast/normal: 200ms in, 160ms out. Out is quicker
   because it is between you and the result, where in is decoration on
   something you already have.

   TWO CURVES, NOT ONE. Arriving decelerates (--ease-out, the app's own
   cubic-bezier(.22,1,.36,1)): it comes in fast and settles, which is how a
   thing that is staying should behave. Leaving accelerates away instead.
   Running the exit on --ease-out was measured at 23% opacity by 40ms and 4%
   by 80ms of its 160, so the whole back half of it played on something
   already invisible -- an 80ms blink wearing a 160ms budget. Scrubbed frame
   by frame, not eyeballed, because a hidden tab does not advance an animation
   clock and the numbers are the only honest way to see the curve. */
@keyframes dzc-in {
  from { opacity: 0; transform: translate3d(-8px, 0, 0) scale(.985); }
  to   { opacity: 1; transform: none; }
}
@keyframes dzc-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translate3d(-10px, 0, 0) scale(.97); }
}
.dzc-squad.is-in, .dzc-bb.is-in, .dzc-army-card.is-in {
  animation: dzc-in var(--duration-normal, 200ms) var(--ease-out, cubic-bezier(.22,1,.36,1)) 1 both;
}
/* forwards, so the card holds its faded-out state for the frames between the
   animation ending and the redraw replacing it. Without it the card snaps back
   to full opacity first and the delete flashes. */
.dzc-squad.is-out, .dzc-bb.is-out, .dzc-army-card.is-out {
  animation: dzc-out 160ms cubic-bezier(.55, 0, 1, .45) 1 forwards;
  pointer-events: none;
}
/* A nested Squad is inside the card of the thing carrying it, so an ancestor
   leaving already takes it with it. Animating both means two overlapping
   fades and a passenger that dims twice as fast as its transport. */
.dzc-squad.is-out .dzc-squad { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .dzc-squad.is-in, .dzc-bb.is-in, .dzc-army-card.is-in,
  .dzc-squad.is-out, .dzc-bb.is-out, .dzc-army-card.is-out { animation: none; }
  /* Still hide it on the way out: leave() resolves immediately under reduced
     motion, but if anything delays the redraw the card must not sit there
     looking alive after you pressed delete. */
  .dzc-squad.is-out, .dzc-bb.is-out, .dzc-army-card.is-out { opacity: 0; }
}

/* A DOT, not a word. Jet, 2026-08-08: "simpler dot picker instead of
   take/taken."

   This is the one-model Squad's Variant control -- a Battle Bus is a Bus or a
   Rocket Bus and never both, so it is a radio. It read "Take" / "Taken",
   which is two different words for one control and the wider of the two set
   the width, so the row shifted every time you pressed it. A filled dot is
   the same fact with nothing to read and nothing to reflow.

   The hit area stays 24px; only the ink is small. */
.dzc-v-pick {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0; cursor: pointer;
  border: 0; background: none; border-radius: 50%;
}
/* Bigger -- Jet, 2026-08-08: "make that selector dot bigger". 13px to 20px,
   in a 30px target. It is the only control on that row and it was reading as
   a bullet point rather than as something to press. */
.dzc-v-pick::before {
  content: ''; width: 20px; height: 20px; border-radius: 50%;
  border: 2px solid var(--line, #d8d0c0); background: var(--surface-2, #fff);
  transition: border-color .12s, background .12s;
}
.dzc-v-pick:hover::before { border-color: var(--acc, #1b3a5c); }
.dzc-v-pick:focus-visible { outline: 2px solid var(--acc, #1b3a5c); outline-offset: 1px; }
.dzc-v-pick.is-on::before {
  border-color: var(--acc, #1b3a5c);
  background: var(--acc, #1b3a5c);
  box-shadow: inset 0 0 0 3px var(--surface-2, #fff);
}
/* Keyword abilities sit under the core stats, in the left column with them --
   they are what the Unit IS, read alongside its numbers rather than after the
   whole weapon block. Jet, 2026-08-07. */
.dzc-sq-rules { margin-top: 4px; }
.dzc-sq-side { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.dzc-sq-body > .dzc-sq-side { flex: 0 1 auto; max-width: 320px; }
.dzc-sq-side > .dzc-sq-stats, .dzc-sq-side > .dzc-sq-rules { margin-top: 0; }
.dzc-sq-side .dzc-stats { width: max-content; max-width: 100%; }
/* The table never shrinks its columns to fit. It scrolls inside its own box,
   so a phone gets every number rather than a squeezed, unreadable version. */
.dzc-sq-wpn { margin-top: 8px; overflow-x: auto; }
.dzc-sq-wpn .dzc-wpn { min-width: 620px; }
.dzc-squad .dzc-variants { margin-top: 3px; }
.dzc-sq-cost {
  font-family: var(--font-numeric, 'Outfit', sans-serif);
  font-weight: 600; font-size: 14px; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.dzc-stepper { display: inline-flex; align-items: center; gap: 2px; }
.dzc-stepper button { width: 24px; height: 24px; border-radius: 4px; cursor: pointer; border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff); font-size: 15px; line-height: 1; }
.dzc-stepper button:hover { border-color: var(--acc); }
/* Carries the title for a disabled stepper button. A disabled control does not
   reliably fire hover and browsers disagree about whether its tooltip shows at
   all; a span always gets the pointer. Laid out as if it were not there. */
.dzc-step-why { display: inline-flex; }
.dzc-stepper b {
  min-width: 20px; text-align: center; font-size: 15px; font-weight: 600;
  font-family: var(--font-numeric, 'Outfit', sans-serif);
  font-variant-numeric: tabular-nums;
}
.dzc-cmdr select { font-size: 11.5px; padding: 3px 6px; border-radius: 4px; border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff); color: var(--ink, #24201c); max-width: 100%; }
.dzc-cmdr { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-2, #6b6459); display: inline-flex; align-items: center; gap: 4px; }

/* ── the Transport a Squad rides in ──
   What is chosen is shown outright, with its symbols; the + opens the chooser.
   A collapsed select could name the Transport and nothing else, and the name
   is the least useful thing about it. */

/* The chooser. Every option says what it offers, how many the cargo needs,
   what that costs, and whether it comes out exactly full (3.2.4). */
.dzc-carry-for { margin: 0 0 10px; font-size: 14px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.dzc-carry-for span { font-size: 12px; color: var(--ink-2, #6b6459); }
.dzc-carry-for .dzc-badge { --badge-px: 34px; --cargo-px: 27px; }
.dzc-carry-head {
  margin: 14px 0 6px;
  font: 700 10px/1 var(--font-condensed, 'Jost', sans-serif);
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2, #6b6459);
}
.dzc-carry-head:first-of-type { margin-top: 0; }
.dzc-carry-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(184px, 100%), 1fr)); gap: 8px; }
/* A Transport already in the Group costs nothing to use, so it reads as the
   cheaper move it is (3.2.4.1). */
.dzc-carry-here { border-left: 3px solid var(--acc); }
.dzc-carry-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 5px;
  padding: 10px; cursor: pointer; text-align: left;
  border: 1px solid var(--line, #d8d0c0); border-radius: 0;
  background: var(--surface-2, #fff); color: inherit;
  transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
}
.dzc-carry-card:hover { border-color: var(--acc); box-shadow: inset 0 0 0 1px var(--acc); }
.dzc-carry-card:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
.dzc-carry-card.is-on { border-color: var(--acc); box-shadow: inset 0 0 0 2px var(--acc); }
.dzc-carry-card img, .dzc-carry-noart {
  width: 100%; height: 56px; object-fit: contain;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-3, #f2ece0); color: var(--ink-2, #9a9184);
}
.dzc-carry-name { font: 700 14px/1.25 var(--font-display, 'Roboto Slab', serif); overflow-wrap: anywhere; }
.dzc-carry-caps .dzc-badge { --badge-px: 34px; --cargo-px: 27px; }
.dzc-carry-sum { display: inline-flex; align-items: baseline; gap: 7px; font-family: var(--font-numeric, 'Outfit', sans-serif); }
.dzc-carry-sum b { font-size: 17px; font-weight: 700; }
.dzc-carry-sum i { font-style: normal; font-size: 13px; color: var(--ink-2, #6b6459); }
.dzc-carry-fit {
  display: flex; align-items: flex-start; gap: 4px;
  font-size: 11px; line-height: 1.35; color: #14532d;
}
.dzc-carry-fit svg { flex: 0 0 auto; margin-top: 1px; }
/* Offered, and marked. One more model makes it legal, so it is a state you
   are allowed to be in on the way somewhere (3.2.4). */
.dzc-carry-card.is-partial .dzc-carry-fit { color: #8a6d1f; }
.dzc-carry-walk .dzc-carry-fit { color: var(--ink-2, #6b6459); }
/* Adding a Group is the main move in this screen, so it is sized like one. */
/* Sticky, so it stays reachable down a long army instead of living at the
   bottom of the list. Smaller than it was, because it no longer has to shout
   from off-screen. */
/* Two adders side by side, equal width, wrapping to one per line on a phone
   where a 50/50 split makes both too narrow to read. The count on the right of
   each is what is actually addable right now, so a disabled button is never a
   mystery. */
.dzc-add-row { display: flex; flex-wrap: wrap; gap: 6px; }
.dzc-add-row > .dzc-add-squad { flex: 1 1 190px; }
/* Outlined, not filled. Same size, same chamfer, same hairline draw on hover --
   only the fill goes, so the two still read as a pair and one of them leads. */
.dzc-add-squad--quiet {
  background: transparent; color: var(--acc-text, var(--acc, #1b3a5c));
  box-shadow: inset 0 0 0 1px var(--line, #d8d0c0);
}
.dzc-add-squad--quiet .dzc-add-squad-i {
  background: color-mix(in srgb, var(--acc, #1b3a5c) 12%, transparent);
  color: var(--acc-text, var(--acc, #1b3a5c));
}
.dzc-add-squad--quiet:hover, .dzc-add-squad--quiet:focus-visible {
  box-shadow: inset 0 0 0 2px var(--acc, #1b3a5c);
}
.dzc-add-squad--quiet:hover .dzc-add-squad-i,
.dzc-add-squad--quiet:focus-visible .dzc-add-squad-i {
  background: color-mix(in srgb, var(--acc, #1b3a5c) 20%, transparent);
}
.dzc-add-squad-n {
  font-style: normal; font-family: var(--font-numeric, 'Outfit', sans-serif);
  font-size: 12px; opacity: .75; margin-left: auto;
}
.dzc-add-squad.is-empty {
  background: transparent; color: var(--ink-2, #6b6459);
  border: 1px dashed var(--line, #d8d0c0); cursor: not-allowed; box-shadow: none;
}
.dzc-add-squad.is-empty:hover { background: transparent; }

/* ── Add Squad ──
   The main action in the builder, and it was a ghost button in the smallest
   size the app has. Now it is the widest solid thing in the Group card, in the
   faction accent, chamfered like the Create button so the two read as the same
   family of "this makes something".

   The hover is a real move, not a tint: the chevron slides out of its box and
   the label slides over to meet it, a gold band sweeps the face behind the
   text, and the whole thing lifts. Every step is transform or opacity, so it
   composites on the GPU and does not reflow anything around it. */
.dzc-add-squad {
  position: relative; overflow: hidden; isolation: isolate;
  display: flex; align-items: center; gap: 10px; width: 100%;
  margin-top: 10px; padding: 12px 16px; cursor: pointer;
  border: 0; border-radius: 0;
  background: var(--acc, #1b3a5c); color: var(--acc-on, #fff);
  font: 700 15px/1 var(--font-ui, 'Jost', sans-serif);
  letter-spacing: .06em; text-transform: uppercase;
  transition: box-shadow 180ms cubic-bezier(.33,0,.67,1);
}
/* The chevron sits in its own tile, which brightens and pushes forward. */
.dzc-add-squad-i {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; flex: 0 0 auto;
  background: rgba(255,255,255,.14);
  transition: transform 260ms cubic-bezier(.34,1.4,.64,1), background 200ms ease;
}
/* NO SWEEP. Jet, 2026-08-07: "remove the long slash in the add squad. the
   spin is fun but the flash is distracting." A gold band travelling across the
   button pulls the eye to the movement rather than to the thing you are about
   to press. The spin on the + stays, and so does the hairline: one draws
   itself along an edge, the other flew across the label. */
/* A hairline that draws itself along the bottom edge. */
.dzc-add-squad::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: var(--gold, #c9a92c); transform: scaleX(0); transform-origin: left;
  transition: transform 320ms cubic-bezier(.33,0,.67,1);
}
.dzc-add-squad:hover, .dzc-add-squad:focus-visible {
  box-shadow: inset 0 0 0 2px var(--gold, #c9a92c);
}
.dzc-add-squad:hover::after, .dzc-add-squad:focus-visible::after { transform: scaleX(1); }
.dzc-add-squad:hover .dzc-add-squad-i,
.dzc-add-squad:focus-visible .dzc-add-squad-i {
  transform: rotate(90deg); background: rgba(255,255,255,.26);
}
.dzc-add-squad:active { filter: brightness(.92); box-shadow: none; }
@media (prefers-reduced-motion: reduce) {
  .dzc-add-squad, .dzc-add-squad::before, .dzc-add-squad::after,
  .dzc-add-squad-i, .dzc-add-squad-t { transition: none; }
  .dzc-add-squad:hover .dzc-add-squad-i { transform: rotate(90deg); }
}

/* Symbols on the picker card, at the size they deserve. Which shape a unit
   fills and which it offers is what decides whether two things can share a
   Group at all, so it is the single most consequential fact on the card. It
   does not go in a little chip beside the others. A triangle also only has
   room for a digit near its base, so shrinking these hid the count, and the
   count is half the symbol. */
.dzc-pick .dzc-transport { margin: 5px 0 1px; gap: 4px; }
.dzc-pick .dzc-badge { --badge-px: 40px; --cargo-px: 32px; }
.dzc-pick .dzc-sep { font-size: 17px; }

.dzc-add-group {
  position: sticky; bottom: 12px; z-index: 3;
  margin-top: 8px; width: 100%;
  padding: 13px 20px;
  font-size: 15px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  background: var(--surface-2, #fff);
  box-shadow: 0 3px 14px rgba(27,26,23,.16);
}

/* ── picker toolbar ── */
.dzc-pick-bar {
  position: sticky; top: 0; z-index: 2;
  padding-bottom: 6px; margin-bottom: 2px;
  background: var(--surface, #faf7f0);
}
.dzc-pick-sorts { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 5px; }
.dzc-pick-sortlab {
  font: 700 10px/1 var(--font-condensed, 'Jost', sans-serif);
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2, #6b6459);
  margin-right: 2px;
}
/* ONE LEFT EDGE FOR THE WHOLE TOOLBAR. Jet, 2026-08-13: "Shit feels...
   misaligned. I don't need a bunch more padding (on desktop at least) but
   maybe some more alignment."

   Five rows and four different left edges, measured: the search field at 25
   (behind a 20px magnifier), the category tabs at 0, the sort chips at 34
   (behind "Sort"), the shape chips at 28 (behind "Fits", which is narrower),
   and the count at 0. Nothing was wrong with any one row; they simply each
   started wherever their own first child ended.

   So the labels stop setting the indent and a gutter does. Everything that
   reads as content -- field, tabs, chips, count -- starts at --pick-gutter,
   and the two labels and the magnifier hang in it. The bar is no wider: the
   gutter is carved out of the row, not added to the bar. */
@media (min-width: 861px) {
  .dzc-pick-bar { --pick-gutter: 34px; }
  /* Minus the row's own 6px gap, so the field lands ON the gutter. */
  .dzc-pick-bar .dzc-search-row > .dzc-i { flex: 0 0 calc(var(--pick-gutter) - 6px); }
  .dzc-pick-bar .dzc-chips,
  .dzc-pick-bar .dzc-pick-results { padding-left: var(--pick-gutter); }
  /* Minus the row's own 4px gap, for the same reason. A fixed width also
     stops "Sort" and "Fits" indenting their rows by different amounts. */
  .dzc-pick-bar .dzc-pick-sortlab {
    flex: 0 0 calc(var(--pick-gutter) - 4px); margin-right: 0;
  }
}
.dzc-chip--sm { padding: 4px 8px; font-size: 11px; }
/* The variant switcher over a weapon table. A lens, not a choice: it changes
   nothing about the Unit, so it sits with the table it filters rather than up
   with the variant blocks, which ARE choices in a Squad. */
.dzc-lens { margin: 8px 0 4px; }
/* The direction arrow sits in a slot that is always there, empty or not, so a
 * sort chip is the same width active as inactive and the row never reflows. */
.dzc-dir { display: inline-block; width: 9px; text-align: center; font-style: normal; }
/* How many units are behind a category tab. Quieter than the label, and it
   inherits colour on the active chip so it does not go grey-on-navy. Numeric
   font, like every other number the user reads as a value. */
.dzc-chip-n {
  margin-left: 5px; font-style: normal; font-family: var(--font-numeric, inherit);
  font-size: .85em; opacity: .6;
}
.dzc-chip.is-active .dzc-chip-n { opacity: .75; }

/* Transport-symbol filters. The glyph IS the label -- these are the six shapes
 * printed on the stat cards, and 3.2.4.2 turns entirely on telling them apart,
 * so a word for the shape would be a worse label than the shape. */
.dzc-pick-shapes { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 5px; }
.dzc-shape-chip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 24px; padding: 0; cursor: pointer;
  border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff);
}
.dzc-shape-chip:hover { border-color: var(--sh); }
/* Active fills the glyph with its own colour -- solid vs hollow is already the
 * app's way of saying "this one, definitely", and the colour must stay the
 * shape's own or the chip stops matching the card. */
.dzc-shape-chip.is-active { border-color: var(--sh); box-shadow: inset 0 0 0 2px var(--sh); }
.dzc-shape-chip.is-active svg path { fill: var(--sh); }

.dzc-pick-results {
  display: flex; align-items: center; gap: 8px; margin-top: 5px; min-height: 22px;
  font-size: 11.5px; color: var(--ink-2, #6b6459);
}
.dzc-pick-results button {
  padding: 2px 8px; cursor: pointer; font-size: 11px;
  border: 1px solid var(--line, #d8d0c0); background: transparent; color: inherit;
}
.dzc-view-toggle {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; cursor: pointer;
  border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff);
  color: var(--ink-2, #5b544a);
}
.dzc-view-toggle:hover { border-color: var(--acc); color: var(--ink, #24201c); }

/* ── the fold ──
 *
 * Sort, the filters and Fits are one row each on a desktop and they cost
 * nothing there, so both wrappers are DISPLAY: CONTENTS: the Sort row and the
 * Fits row go on being children of the bar, at the spacing they always had,
 * and above 768px neither wrapper may as well exist. They only become boxes on
 * a phone, where what they become is a box that is shut. Two of them because
 * the animation needs an inner element to overflow-hide while the outer one
 * goes from 0fr to 1fr.
 *
 * The measurement that put it here: at 390 wide, the bar was 363px tall and
 * the first unit card started 467px down an 844px screen. Over half the
 * viewport was controls, and the picker's job is to show you units. */
.dzc-pick-more, .dzc-pick-more-in { display: contents; }
.dzc-pick-more-btn { display: none; }

/* The category tabs are the last item in the flex row and take a whole line to
   themselves, which is exactly where they already were -- they just used to be
   their own element to do it. Row gap has to be nothing, or the whole toolbar
   drops 6px on a desktop that asked for none. Measured either side at 900,
   1100 and 1280: every child of the bar lands on the same pixel it did. */
#dzc-picker-body .dzc-pick-top { row-gap: 0; }
.dzc-pick-top > .dzc-chips { order: 9; flex: 1 1 100%; min-width: 0; }

@media (max-width: 768px) {
  /* THE BUTTONS SIT BESIDE THE FIELD, and the tabs get the next line whole.
     They used to be pushed down among the tabs, because at 44px square the
     pair left 230px for "Search units, variants, weapons or rules" -- Jet's
     copy, whose whole job is to say what you can type -- and clipped it at
     390. At 36 they leave 268 and the placeholder sets in 240, measured in
     Jost at 13px with the field's own padding counted, so it has 28px spare
     and the line it used to cost is gone.

     Which also gives the tabs a line of their own, and all five of a faction's
     categories fit it: 332px of chips and gaps against 348 of column. That is
     the second of the two rows this block used to spend on the same content.
     The desktop rule below already puts the strip on its own full-width line;
     the phone now wants exactly that, so the override that broke it up is
     gone. */
  #dzc-picker-body .dzc-pick-top { row-gap: 4px; }
  /* The magnifier goes: it was already going at 560, for the reason that
     covers this band too -- a type="search" box with that placeholder in it
     does not need a picture of a magnifying glass to say it is a search --
     and the 28px of slack above is measured without it. */
  #dzc-picker-body .dzc-pick-top > .dzc-i { display: none; }

  /* Everything else folds. 0fr to 1fr rather than a max-height guess, so the
     row opens to exactly its own height whatever is in it -- the filter chips
     are built from what this faction actually has and there are between six
     and eight of them. */
  #dzc-picker-body .dzc-pick-more-btn { display: inline-flex; }
  .dzc-pick-more {
    display: grid; grid-template-rows: 0fr;
    transition: grid-template-rows .16s cubic-bezier(.33, 0, .67, 1);
  }
  .dzc-pick-more.is-open { grid-template-rows: 1fr; }
  /* Shut means shut for a keyboard too: a zero-height box still hands focus to
     every chip inside it, and tabbing into controls nobody can see is the same
     bug as the filter that cuts the list from behind a closed row. */
  /* ONE wrapping flow, not three stacked groups. Sort, the filters and Fits are
     chips of the same size doing the same job, and a line each left the Fits
     row two thirds empty and spent a whole row saying so. Both wrappers go to
     display: contents so all seventeen chips and their two labels wrap
     together, which is the same trick the category tabs already use above. */
  .dzc-pick-more-in {
    display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
    padding-top: 4px; overflow: hidden; min-height: 0;
    visibility: hidden; transition: visibility 0s .16s;
  }
  .dzc-pick-more.is-open .dzc-pick-more-in { visibility: visible; transition-delay: 0s; }
  #dzc-picker-body .dzc-pick-sorts,
  #dzc-picker-body .dzc-pick-shapes { display: contents; }

  /* Open is a state you can see without opening it, and so is a filter left on
     from last time -- the count is on the button. */
  .dzc-pick-more-btn { gap: 3px; padding: 0 8px; width: auto; min-width: 44px; }
  .dzc-pick-more-btn.is-open { border-color: var(--acc, #1b3a5c); color: var(--ink, #24201c); }
  .dzc-pick-more-btn.is-active {
    background: var(--acc, #1b3a5c); border-color: var(--acc, #1b3a5c); color: var(--acc-on, #fff);
  }
  .dzc-more-n {
    font: 700 11px/1 var(--font-numeric, inherit); font-style: normal;
  }
  .dzc-more-n:empty { display: none; }

  /* Tighter than the 44px pass leaves them. A category tab, a sort, a filter
     and a Fits shape are a dense strip you read across, not the thing you
     press to do something -- Add, Clear and the two toolbar buttons all stay
     at 44 -- and at 44 apiece the strip cost more of the phone than the units
     it filters. 36 is still twelve past the 24px the guideline asks of a
     control this size. */
  #dzc-picker-body .dzc-pick-bar .dzc-chip,
  #dzc-picker-body .dzc-pick-bar .dzc-chip--sm,
  #dzc-picker-body .dzc-pick-bar .dzc-view-toggle,
  #dzc-picker-body .dzc-shape-chip { min-height: 36px; min-width: 0; }
  #dzc-picker-body .dzc-chip { padding: 4px 6px; font-size: 11px; }
  #dzc-picker-body .dzc-chip--sm { padding: 4px 6px; font-size: 11px; }
  #dzc-picker-body .dzc-chip-n { margin-left: 3px; }
  #dzc-picker-body .dzc-shape-chip { width: 34px; height: 36px; }
  #dzc-picker-body .dzc-view-toggle { width: 36px; height: 36px; }
  #dzc-picker-body .dzc-pick-more-btn { min-width: 36px; padding: 0 7px; }
  #dzc-picker-body .dzc-pick-top { row-gap: 4px; column-gap: 4px; }
  #dzc-picker-body .dzc-pick-bar { padding-bottom: 4px; }
  #dzc-picker-body .dzc-pick-results { margin-top: 4px; }
}

/* Below 390 the sum stops working, on both halves of it at once.

   The modal is 40px narrower than the viewport by the time the overlay and the
   body have taken their padding, so at 360 the pair of buttons leaves the field
   238px for a placeholder that sets in 242, and at 320 it leaves 200. The words
   do not give way.

   And there is nothing to win by cramming them anyway: a faction's five tabs
   want 348px, which a 360 phone does not have either, so the top of this
   toolbar is three lines below 390 whatever happens on the first one. The
   buttons go back down among the tabs -- which is what they did at every width
   until this build -- and the strip goes back to DISPLAY: CONTENTS so they ride
   at the end of the last tab line rather than claiming a line of their own.
   Measured at 320, 360 and 375: every one of them packs shorter this way than
   with the buttons moved up. */
@media (max-width: 389px) {
  #dzc-picker-body .dzc-pick-top > .dzc-search { flex: 1 1 100%; }
  #dzc-picker-body .dzc-pick-top > .dzc-chips { display: contents; }
}
@media (prefers-reduced-motion: reduce) {
  .dzc-pick-more, .dzc-pick-more-in { transition: none; }
}

/* List view: one row per unit, for scanning names and prices rather than
   comparing cards. */
.dzc-pick-list.is-list { grid-template-columns: 1fr; gap: 3px; }
.dzc-pick-list.is-list .dzc-pick { flex-direction: row; align-items: center; gap: 10px; padding: 6px 8px; }
.dzc-pick-list.is-list .dzc-pick-open { display: flex; align-items: center; gap: 10px; flex: 1 1 auto; }
.dzc-pick-list.is-list .dzc-pick-art,
.dzc-pick-list.is-list .dzc-pick-noart { width: 76px; height: 56px; margin: 0; flex: 0 0 auto; }
.dzc-pick-list.is-list .dzc-pick-head { flex: 1 1 auto; min-width: 0; }
.dzc-pick-list.is-list .dzc-stats,
.dzc-pick-list.is-list .dzc-pick-wpns,
.dzc-pick-list.is-list .dzc-pick-rules,
.dzc-pick-list.is-list .dzc-pick-variants { display: none; }
.dzc-pick-list.is-list .dzc-pick-add { width: auto; margin: 0; padding: 7px 14px; flex: 0 0 auto; }
.dzc-pick-list.is-list .dzc-pick-blocked { margin: 0; flex: 0 0 auto; max-width: 46%; }

.dzc-icon-btn { flex: 0 0 auto; width: 24px; height: 24px; border: 0; border-radius: 4px; cursor: pointer; background: transparent; color: var(--ink-2, #6b6459); font-size: 18px; line-height: 1; }
.dzc-icon-btn:hover { background: var(--err-bg, #fde8e6); color: var(--danger, #b3261e); }

/* ── new-army dialog ── */
.dzc-field { display: block; margin-bottom: 12px; }
.dzc-field span { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2, #6b6459); margin-bottom: 4px; }
.dzc-field input, .dzc-field select { width: 100%; padding: 8px 10px; font-size: 14px; border-radius: 6px; border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff); color: var(--ink, #24201c); }
.dzc-field-note { font-size: 12px; color: var(--ink-2, #6b6459); margin: 0; }

/* ── unit picker ──
 * Four across at the modal's full width. Everything you need to choose is on
 * the card; the card body opens the stat card, Add is its own target. */
/* A grid means a grid: cards align top AND bottom, and every Add button in a
   row sits on the same line. The card body takes the slack so the button stays
   pinned to the bottom. */
.dzc-pick-list {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(228px, 100%), 1fr));
  gap: 6px; margin-top: 6px;
}
.dzc-pick {
  position: relative;
  display: flex; flex-direction: column; min-width: 0; padding: 7px;
  border: 1px solid var(--line, #d8d0c0); border-radius: 8px;
  background: var(--surface-2, #fff);
}
.dzc-pick:hover { border-color: var(--acc); }
/* Rare/Unique sits in the top-left corner over the art, where it is the first
   thing scanned, rather than trailing a name that may have wrapped. */
.dzc-pick-flags {
  position: absolute; top: 5px; left: 5px; z-index: 1;
  display: flex; flex-wrap: wrap; gap: 3px;
}
.dzc-pick-open { display: block; flex: 1 1 auto; min-width: 0; cursor: pointer; }
/* BIGGER. Jet, 2026-08-07: "when you're in the ADD SQUAD searcher... we must
   make the images bigger." 52px of a model on a 253px card is a thumbnail of a
   thumbnail. You are choosing between models you cannot see. 96, which is
   most of the card's width and still leaves the stats and the guns above the
   fold. */
.dzc-pick-art, .dzc-pick-noart {
  display: block; width: 100%; height: 96px; object-fit: contain; margin-bottom: 3px;
}
.dzc-pick-head { display: flex; align-items: flex-start; gap: 6px; }
.dzc-pick-name {
  flex: 1 1 auto; min-width: 0; font-weight: 600; font-size: 13.5px;
  line-height: 1.25; overflow-wrap: anywhere;
}
.dzc-pick-name .dzc-flag { margin-left: 4px; vertical-align: 1px; }
.dzc-pick-cost {
  flex: 0 0 auto;
  font: 600 17px/1 var(--font-numeric, 'Outfit', sans-serif);
  font-variant-numeric: tabular-nums;
  color: var(--acc-ink, #1b3a5c);
}
.dzc-pick-cost { text-align: right; }
.dzc-pick-cost small { font-size: 10px; font-weight: 600; margin-left: 1px; }
/* The arithmetic behind the total, under it and quieter. Only drawn when the
   Squad starts above one model, because "1 × 35" under "35pts" is noise. */
.dzc-pick-each {
  display: block; margin-top: 1px; font-style: normal;
  font: 500 10px/1 var(--font-numeric, 'Outfit', sans-serif);
  color: var(--ink-2, #6b6459); white-space: nowrap;
}
.dzc-pick-meta { display: block; margin-top: 1px; font-size: 11px; color: var(--ink-2, #6b6459); }

/* Facts in a row are separated by space, not by punctuation. The interpunct
   budget is two in the whole app, and a meta line spent four of them on its
   own. Spacing does the same job and reads quieter. */
.dzc-pick-meta span + span::before,
.dzc-b-sub span + span::before,
.dzc-b-sub span + button::before,
.dzc-detail-meta span + span::before,
.dzc-coll-meta span + span::before,
.pr-sub span + span::before { content: '   '; white-space: pre; }
.dzc-detail-meta .dzc-flag + .dzc-flag::before,
.dzc-detail-meta span + .dzc-flag::before { content: ' '; white-space: pre; }
.dzc-pick-stats { margin-top: 4px; }

/* Two columns, three rows. Every cell carries its icon AND its word AND its
   value. An icon alone does not tell you what a number means. Same shape as
   the Dropfleet builder's .stat-grid. */
/* minmax(0, 1fr), not 1fr. A bare 1fr is minmax(AUTO, 1fr), so every column is
   floored at its content's min-content width. And .dzc-stat-k refuses to break
   inside a word, so DEFENCE and BRAVERY have a floor they cannot go under.
   Three of those were wider than a reference card, the grid overflowed, and the
   card clipped it: the Legionnaire Weapon Team read "5+ DEFENC". */
.dzc-stats { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 2px; }
/* Three columns, not a flex row: icon, value, label.

   As a flex row every cell packed left and the value took whatever width it
   needed, so 2" and 18" and 5+ each started the label at a different x and no
   two numbers in the grid lined up with each other. A stat grid you cannot
   read down a column is a list of six unrelated facts. The value column is
   fixed, so the digits start on the same line in every cell and the labels do
   too.

   In PX, not em. An em track resolves against whatever font-size the cell has
   inherited, which is not the same on a reference card, a Squad row and the
   picker. So the label started at a different x on each of them and the
   column only looked aligned within one screen at a time.

   38px holds 18" and 5+ and 3+. A Behemoth's damage track prints "9, 14, 18",
   which is wider than any of them; it WRAPS inside its own column rather than
   widening the track, because one card with a long value must not move the
   label on every card beside it. */
.dzc-stat {
  display: grid; grid-template-columns: 18px 44px minmax(0, 1fr);
  align-items: baseline; gap: 5px; min-width: 0;
  padding: 4px 6px; border-radius: 4px;
  background: var(--surface-3, #f2ece0);
  border: 1px solid var(--line, #e3dcce);
}
.dzc-stat-i { flex: 0 0 auto; align-self: center; display: inline-flex; opacity: .8; }
.dzc-stat-v {
  font-family: var(--font-numeric, 'Outfit', sans-serif);
  /* 600, not 700: heavier closes the counters on 3 / 5 / 8 at this size. The
     Dropfleet stat cell carries the same note. */
  font-size: 16px; font-weight: 600; line-height: 1.15;
  min-width: 0; overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink, #24201c);
}
.dzc-stat-k {
  min-width: 0;
  /* Wrap between words, never inside one. "DAMAGE POINTS" over two lines is
     fine; "ARMOU / R" is not, and that is what anywhere-breaking produced in
     the narrower reference cards. */
  overflow-wrap: normal; word-break: keep-all; hyphens: none;
  font-family: var(--font-condensed, 'Jost', sans-serif);
  font-size: 10.5px; font-weight: 700; line-height: 1.15;
  text-transform: uppercase; letter-spacing: .02em;
  color: var(--ink-2, #6b6459);
}
/* A unit shows only the stats its card prints, so the grid fits the count:
   three across for a Vehicle or Aircraft (Mv A DP), and Infantry's five run
   three then two. */
.dzc-stats.n-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.dzc-stats.n-5 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* COMPACT. The picker card, where three stats share 237px.
   
   The wide cell above is `18px icon | 44px value | label`, and those fixed
   tracks plus their gaps and padding want 84px before the label gets a single
   pixel. A compact cell is 78px. So the label track collapsed to 1px against
   the 15px it needed and rendered as a sliver of its first letter -- "Mv" as a
   vertical scratch, and the value overflowing its own box on top of it. Every
   Vehicle in the picker read like that.
   
   Stacked instead: icon and value on one line, the code under them across the
   full width of the cell. The cell now needs the wider of "24”" and "DP",
   about 46px, in a box of 78. Values still line up card to card because the
   icon track is still fixed -- the 44px value track only ever existed to line
   the LABELS up, and at this width there is no label beside them to line up
   with. Nothing is dropped and nothing shrank: the code is bigger than it
   was. */
.dzc-stats.is-compact { gap: 2px; }
.dzc-stats.is-compact .dzc-stat {
  display: grid; grid-template-columns: 16px minmax(0, 1fr);
  align-items: center; gap: 0 4px; padding: 3px 5px;
}
.dzc-stats.is-compact .dzc-stat-i { grid-column: 1; grid-row: 1; }
.dzc-stats.is-compact .dzc-stat-v { grid-column: 2; grid-row: 1; font-size: 14px; }
.dzc-stats.is-compact .dzc-stat-k {
  grid-column: 1 / -1; grid-row: 2; font-size: 9.5px; letter-spacing: .04em;
}

/* The detail view only (not .is-compact. The picker still needs four cards
   across, spread wide is the point there). A short table going down, icon /
   value / label lined up in their own columns across rows, left-aligned and
   only as wide as the longest label. Not stretched to fill the row like the
   picker's spread grid. .dzc-stat becomes `display: contents` so its three
   spans join the parent grid directly instead of boxing them off in a row;
   that also voids the chip background/border/padding above for free, since a
   `display: contents` box never paints one. Frees the rest of the row for
   whatever sits beside it (weapons, rules) instead of stats owning the
   full width above them. */
/* A GRID, two cells across.
   
   It was one column going down: five stats, five rows, a tall thin strip of
   numbers. Jet has asked for a grid twice now -- "we need words + icons, and i
   want it in a 2x3 grid" -- and with the guns beside it rather than under it
   a column that tall sets the height of the whole Squad row for no reason.
   Two across turns Infantry's five into three rows and a Vehicle's three into
   two, and it is the same shape as the Dropfleet stat grid it came from.
   
   Each cell keeps its own icon / value / label tracks, so the numbers still
   line up down each column. `display: contents` is gone with the single
   column: a cell has to be a box again to be a cell. */
/* auto-fit, not a hard 2 -- and width: 100%, not max-content.
   
   A fixed two columns at max-content width made the reference card wider than
   a 320px phone: the card sizes to its contents, so a grid that asks for its
   own natural width takes the card off the screen with it. auto-fit gives two
   across wherever 140px each will fit and one where it will not, and the box
   never asks for more than it is given. */
.dzc-stats:not(.is-compact) {
  /* ONE COLUMN, ALWAYS. Jet, 2026-08-07: "except the core unit stats. that i
     still want stacked, Vertically, ALWAYs vertically." Two across was read as
     a grid to scan; a unit's stats are a block to read down. */
  display: grid; grid-template-columns: minmax(0, 1fr);
  row-gap: 2px; width: 100%; max-width: 100%;
}
.dzc-stats:not(.is-compact) .dzc-stat {
  grid-template-columns: 16px 34px minmax(0, 1fr); gap: 4px;
  padding: 2px 5px;
}
/* LEFT, not right. Right-aligning put the digits at three different x: 6" is
   wider than 3, so its 6 sat further left than the bare numbers under it and
   the column read as ragged. The digit is the thing being compared. */
.dzc-stats:not(.is-compact) .dzc-stat-v { text-align: left; }
.dzc-stats:not(.is-compact) .dzc-stat-k { align-self: center; }

.dzc-pick-wpns { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 4px; }
.dzc-pick-wpn {
  display: inline-flex; align-items: center; gap: 3px; padding: 1px 5px;
  border-radius: 3px; background: var(--surface-3, #f2ece0);
  font-size: 10.5px; line-height: 1.5; color: var(--ink-2, #5b544a);
}
.dzc-pick-wpn svg { flex: 0 0 auto; }
.dzc-pick-variants { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 4px; }
.dzc-pick-vchip {
  padding: 1px 6px; font-size: 10.5px; line-height: 1.5; font-weight: 600;
  color: #7a4a06; background: rgba(201,169,44,.16);
  border: 1px solid rgba(201,169,44,.55);
}
.dzc-pick-rules { margin-top: 3px; }
.dzc-pick-rules .dzc-rule { margin: 2px 2px 0 0; padding: 1px 5px; font-size: 10.5px; }

/* The add button is the point of the card, so it reads like it. Deco corners:
   cut top-left and bottom-right, with a gold hairline picking out the cut. */
.dzc-pick-add {
  position: relative;
  display: flex; align-items: center; justify-content: center; gap: 5px;
  width: 100%; margin-top: 6px; padding: 9px 12px;
  border: 0; border-radius: 0; cursor: pointer;
  background: var(--acc-ink, #1b3a5c); color: #fff;
  font: 700 14px/1 var(--font-ui, 'Jost', sans-serif);
  letter-spacing: .04em; text-transform: uppercase;
  transition: background .14s var(--ease-deco, cubic-bezier(.33,0,.67,1)), color .14s;
}
.dzc-pick-add::before,
.dzc-pick-add::after {
  content: ''; position: absolute; width: 11px; height: 11px;
  border-style: solid; border-color: var(--gold, #c9a92c); opacity: .55;
  pointer-events: none;
}
.dzc-pick-add::before { top: 2px; right: 2px; border-width: 1px 1px 0 0; }
.dzc-pick-add::after { bottom: 2px; left: 2px; border-width: 0 0 1px 1px; }
.dzc-pick-add:hover { background: var(--gold, #c9a92c); color: #1b1710; }
.dzc-pick-add:hover::before, .dzc-pick-add:hover::after { border-color: #1b1710; opacity: .5; }

/* ── enforcement affordances ── */
.dzc-pick.is-blocked { opacity: .6; }
.dzc-pick.is-blocked:hover { border-color: var(--line, #d8d0c0); }
/* A Commander Level the agreed size does not reach, dimmed the same amount as
   a Unit you may not take. Same refusal, so it should read the same. */
.dzc-cmdr-opt.is-blocked { opacity: .6; }
.dzc-pick-blocked {
  display: flex; align-items: center; gap: 4px; margin-top: 8px; padding: 8px 10px;
  border-radius: 5px; background: rgba(138, 75, 18, .09);
  font-size: 11px; font-weight: 600; color: #8a4b12;
}
/* Search and the category chips are one row's worth of function, so they get
   one row. Everything above the first card is chrome and it was taking three. */
/* The row grows, not just the input inside it. Without this the row sized to
   the input's 280px basis and stopped, so the Collection's box read "Search
   units, variants, wea" with a third of the toolbar empty beside it. */
.dzc-search-row { display: flex; align-items: center; gap: 6px; flex: 1 1 280px; min-width: 0; }
.dzc-search-row .dzc-i { flex: 0 0 auto; color: var(--ink-2, #6b6459); }
/* On a phone the magnifier goes and the field keeps its width. The placeholder
   is Jet's, and its whole job is to say what you can type, "Search units,
   variants, weapons or rules", so it is the icon that gives way, not the
   words: a type="search" field with that in it does not need a picture of a
   magnifying glass to say it is a search. */
@media (max-width: 560px) { .dzc-search-row > .dzc-i { display: none; } }
#dzc-picker-body .dzc-search-row { flex-wrap: wrap; }
#dzc-picker-body .dzc-search { flex: 1 1 200px; padding: 6px 10px; font-size: 13px; }
#dzc-picker-body .dzc-chips { display: flex; gap: 4px; margin: 0; }
#dzc-picker-body .dzc-chip { padding: 5px 9px; font-size: 11.5px; }
/* 320 is the narrowest the layout check measures and the field has the whole
   line to itself there, so the last thing between the placeholder and its own
   box is the field's own padding. It gives way; the words do not. */
@media (max-width: 560px) { #dzc-picker-body .dzc-search { padding-inline: 6px; } }
#dzc-picker .modal-header { padding-block: 10px; }
#dzc-picker .modal-body { padding-top: 8px; }
.dzc-stepper button:disabled { opacity: .35; cursor: not-allowed; }
.dzc-stepper.is-derived { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; background: var(--surface-3, #f2ece0); color: var(--ink-2, #6b6459); }
/* NOT italic. It marked a Transport Squad back when a Transport was a chip on
   somebody else's row and needed telling apart from it. It is a card of its
   own now, at the top of the Group, with the line coming out of it -- the
   position says Transport, and a slanted serif said nothing except that this
   name was different from the others in a way you could not name. */
/* A Squad in the army shows the same facts as its picker card, so a unit does
   not become anonymous the moment you take it. */
.dzc-facts { margin: 2px 0 4px; }
.dzc-facts .dzc-pick-stats,
.dzc-facts .dzc-pick-wpns { margin-top: 3px; }
.dzc-cmdr-tag { display: inline-flex; align-items: center; gap: 2px; margin-left: 6px; padding: 1px 5px; border-radius: 3px; background: var(--gold, #c9a92c); color: #221c00; font-size: 10px; font-weight: 700; }
.dzc-i { vertical-align: middle; flex: 0 0 auto; }

/* Refusals appear as a bar, not a dialog: a rule explanation should not be
   something you have to dismiss before carrying on. */
.dzc-toast {
  position: fixed; left: 50%; bottom: 20px; transform: translate(-50%, 12px);
  z-index: 9500; display: flex; align-items: center; gap: 9px;
  max-width: min(560px, calc(100vw - 24px)); padding: 11px 15px; border-radius: 8px;
  background: #2b2118; color: #fff8ec; font-size: 13px; line-height: 1.45;
  box-shadow: 0 10px 30px rgba(0,0,0,.28);
  opacity: 0; visibility: hidden; transition: opacity .16s, transform .16s, visibility .16s;
}
.dzc-toast.is-on { opacity: 1; visibility: visible; transform: translate(-50%, 0); }

/* A <select> sizes itself to its widest OPTION, so "Bear APC × 1 — cannot be
   full (carries 4, needs 6)" made the row 440px wide and the page scrolled
   sideways at 375. Constrain the control and let the row wrap instead; the
   option text stays complete inside the open dropdown, so nothing is lost. */
/* Transport and Commander each get their own line. */
.dzc-carry, .dzc-cmdr { flex: 1 1 100%; min-width: 0; }
.dzc-carry select, .dzc-cmdr select { flex: 1 1 auto; width: 100%; min-width: 0; max-width: 320px; }

/* The builder column is capped so a squad row never has to stretch across a
   full-width desktop viewport. */
/* 640px was right when the builder was one column. With a rail beside it the
   Groups need their own width, or a Squad row is squeezed into 340px. */
.dzc-builder { max-width: 1600px; }

/* ── narrowing down ──
   One responsive app, not two builds. The same markup gets less indent, less
   padding and smaller art as the screen narrows; nothing is removed, and the
   weapon table scrolls inside its own box rather than being squeezed. */
/* NESTING COSTS TWICE, and on a phone it is the whole budget.
 *
 * Every level of carry spends --rider-step indenting the row, and then the
 * rider's own body starts another 44px in to clear the line its carrier sent
 * down. At 52px that is 96px per level before a single stat is drawn, and a
 * Raven carrying a Buggy carrying Legionnaires is two of them: 192 of a 320px
 * phone gone. Measured there, the page scrolled sideways to 455.
 *
 * --rider-step is the lever, because everything in the carry line derives from
 * it and the two things that must not move do not depend on it: the arm still
 * starts at --mini-x - --carry-gap in the rider's own coordinates, and it is
 * still --carry-gap + 22 long. Shrinking it pulls --carry-x in with it, which
 * takes the carrier's own body indent down by the same amount again.
 *
 * The picture goes with it. It has to: --mini-h is what the drop and the arm
 * are measured against, so a smaller box moves the line rather than leaving it
 * pointing at paper. */
@media (max-width: 860px) {
  .dzc-builder { --sq-indent: 14px; }
  .dzc-group-card { padding: 12px; }
  /* The step up on desktop carries down, proportionally: 84x60 -> 100x72. */
  .dzc-squad { --mini-w: 100px; --mini-h: 72px; --rider-step: 44px; }
  button.dzc-sq-name { font-size: 16px; }
  .dzc-sq-cap .dzc-badge { --badge-px: 38px; --cargo-px: 30px; }
}

@media (max-width: 640px) {
  .dzc-b-head { gap: 8px; }
  .dzc-builder { --sq-indent: 8px; }
  .dzc-group-card { padding: 10px; }
  .dzc-squad { --mini-w: 86px; --mini-h: 62px; --rider-step: 36px; }
  .dzc-squad .dzc-squad { padding-left: 8px; }
  .dzc-sq-main { gap: 8px; }
  /* The controls drop to their own full-width row and spread out: at this
     width they were being crushed against the name by margin-left:auto. */
  .dzc-sq-ctl { flex: 1 1 100%; margin-left: 0; justify-content: space-between; }
  .dzc-sq-id { flex: 1 1 140px; }
  /* NO SECOND SIZE FOR THE PICTURE. It is 100% of .dzc-sq-minis and that box
     is what --mini-w/--mini-h set, three lines up. A literal here is the bug
     the comment on .dzc-sq-minis describes and was only half fixed: 56x42 in
     an 86x62 box left the photograph in the top-left corner of a box half
     again its size, and the carry line -- which is measured off the BOX --
     pointed at 31px into a 42px image. Every phone, every Squad. */
  button.dzc-sq-name { font-size: 15px; }
  .dzc-g-space { gap: 8px 16px; }
  .dzc-space-n b { font-size: 22px; }
  .dzc-ratios { gap: 6px; }
  .dzc-ratio { flex: 1 1 calc(50% - 3px); }
  /* These rows used to go flex-wrap: nowrap with a 3px scrollbar, to keep the
     chrome above the cards down to one line each. What that actually did was
     put "Support" and "Transport" past the right edge of a 375px screen behind
     a scrollbar three pixels tall: a filter you cannot see is a filter you do
     not have, and on a narrow desktop window there is no swipe to find it
     with. They wrap. The row grows a line and every chip is on screen, which
     is the rule (CLAUDE.md §4) and was the rule when the scroller was written.

     Nothing shrinks to buy the space back. Chips are min-height 44px on a
     phone and that is deliberate, "rows get taller, that is the trade". */
  .dzc-chip, .dzc-shape-chip { flex: 0 0 auto; }
  /* The three numbers a Level buys take their own row rather than squeezing
     the Level name to nothing. Insignia, Level, price and Add first, then
     what you get for it underneath. */
  .dzc-cmdr-opt { flex-wrap: wrap; }
  .dzc-cmdr-opt-pts { order: 1; margin-left: auto; }
  /* Add and the refusal that stands in for it both sit after the price. Only
     .btn was ordered at first, so a Level out of reach put "Battle and up"
     BEFORE its own price and the blocked rows read in a different order from
     the ones above them. */
  .dzc-cmdr-opt .btn, .dzc-cmdr-opt .dzc-pick-blocked { order: 2; }
  .dzc-cmdr-buys { order: 3; flex: 1 1 100%; }
}

@media (max-width: 420px) {
  .dzc-builder { --sq-indent: 4px; }
  /* The narrowest case in the harness, and the one a three-level chain has to
     survive: 28px of step leaves the carrier's own body starting at 72 rather
     than 96, and a rider's at 44 either way. */
  .dzc-squad { --mini-w: 74px; --mini-h: 53px; --rider-step: 28px; }
  /* NO HEIGHT HERE EITHER, and this one was doing real damage: 96px of
     photograph in a 46px box, so on the narrowest phone the picture stood
     twice the height of the box that is supposed to bound it and hung 50px
     into the row beneath, while the carry line -- measured off --mini-h --
     aimed at 23px into a 96px image. .dzc-sq-art is 100% of .dzc-sq-minis and
     that is the only place its size is allowed to be set. */
  .dzc-sq-cap .dzc-badge { --badge-px: 34px; --cargo-px: 27px; }
  .dzc-pick .dzc-badge { --badge-px: 36px; --cargo-px: 29px; }
  .dzc-add-squad { padding: 11px 12px; font-size: 14px; }
}

/* The squad name is the way into its stats, weapons and rules, and it is set
   in the display face at the size the unit view gives it. A Squad in your
   army is a Unit, not a line item. */
button.dzc-sq-name {
  border: 0; background: transparent; padding: 0; cursor: pointer; text-align: left;
  font: 700 17px/1.25 var(--font-display, 'Roboto Slab', serif); color: var(--ink, #24201c);
  overflow-wrap: anywhere;
  border-bottom: 1px dotted var(--ink-2, #9a9285);
}
button.dzc-sq-name:hover { color: var(--acc-text, var(--acc)); border-bottom-color: var(--acc-text, var(--acc)); }
button.dzc-sq-name:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }

/* ── weapon upgrades (3.2.3) ── */
/* The block that used to live here is gone with upgradesHtml: an upgrade is
   bought on the gun itself now, where the price IS the button (f7b3d47). Only
   the card's own note survives, because the reference has no buttons for it to
   sit over -- so it sits under the table, marked as the card's words rather
   than the app's. */
.dzc-upg-note {
  margin: 6px 0 0; font-size: 12.5px; font-style: italic;
  color: var(--ink-2, #6b6459);
}

/* ── settings / sync dialogs ── */
/* Settings are grouped, and a setting is a name plus a control. The
   explanation is the tooltip, never a caption underneath. */
.dzc-set-group { padding: 8px 0 10px; border-bottom: 1px solid var(--line, #eee5d6); }
.dzc-set-group:last-child { border-bottom: 0; }
.dzc-set-title {
  margin-bottom: 4px;
  font: 700 10.5px/1.4 var(--font-condensed, 'Jost', sans-serif);
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2, #6b6459);
}
.dzc-set-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 5px 0; }
.dzc-set-row > span { font-size: 14px; }
.dzc-set-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 5px 0; font-size: 14px; cursor: pointer;
}
.dzc-set-toggle input { width: 17px; height: 17px; accent-color: var(--acc-ink, #1b3a5c); cursor: pointer; }
.dzc-set-note { margin: 0 0 6px; font-size: 12px; color: var(--ink-2, #6b6459); }
.dzc-set-actions { display: flex; flex-wrap: wrap; gap: 8px; padding-bottom: 10px; }
/* Import. A backup is long, so the box is tall enough to see you have pasted
   something and monospaced so a truncated paste is obvious. */
.dzc-import-text {
  width: 100%; resize: vertical; min-height: 96px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px;
  overflow-wrap: anywhere;
}
.dzc-import-list { margin: 0; padding-left: 18px; font-size: 12px; color: var(--ink-2, #6b6459); }
.dzc-import-list li { margin-bottom: 3px; }
.dzc-seg { display: inline-flex; border: 1px solid var(--line, #d8d0c0); border-radius: 6px; overflow: hidden; flex: 0 0 auto; }
.dzc-seg button { padding: 6px 14px; font-size: 12.5px; border: 0; cursor: pointer; background: transparent; color: var(--ink, #24201c); }
.dzc-seg button.is-on { background: var(--navy, #1b3a5c); color: #fff; }
/* The token is the whole credential, so it is set to be read aloud accurately. */
.dzc-token { font: 600 15px/1.5 var(--font-ui, 'Jost', sans-serif); letter-spacing: .02em; padding: 10px 12px; border-radius: 6px; background: var(--surface-3, #f2ece0); overflow-wrap: anywhere; }

/* ═══ Play Mode ═══
   Sized for a phone on a table: big tap targets, no hover-only affordances. */
.dzc-play-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
/* EVERY NUMBER ON THIS SCREEN IS TABULAR.
   The counters are what change, and in a proportional face a 1 is narrower
   than a 4 -- so advancing the Round moved the button you had just pressed by
   two pixels, and stepping CP from 0 to 4 nudged the label beside it. Small,
   and still the thing this screen was asked not to do. Equal-width digits mean
   a value can change without anything beside it moving. */
.dzc-play, .dzc-play input { font-variant-numeric: tabular-nums; }
/* AND AGAIN, AFTER THE SHORTHANDS.
   `font:` is a shorthand that resets font-variant-numeric to normal, so the
   two counters set with it -- the Round heading and the Command Point /
   Pass Token value -- threw the inherited setting away and went back to
   proportional digits. Which is exactly where the last pixel of movement was
   hiding: stepping the CP cap from /0 to /4 narrowed that <i> by one pixel.
   Declared after the shorthands rather than folded into them, so the reason
   it is a separate line is visible.

   It is not enough on its own. font-variant-numeric only does anything if the
   face actually ships tabular figures, and the display face here does not, so
   the "/4" beside a Command Point count still measured a pixel narrower than
   "/0". A held-open well is the fix that does not depend on a font feature;
   the property stays because on a face that has them it is the better one. */
.dzc-round b, .dzc-pcard-v { font-variant-numeric: tabular-nums; }
.dzc-pcard-v i { display: inline-block; min-width: 24px; }
.dzc-round { display: flex; align-items: center; gap: 10px; }
.dzc-round button { width: 36px; height: 36px; border-radius: 8px; cursor: pointer; border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff); display: grid; place-items: center; }
/* Wide enough for "Round 10", so reaching double figures does not push the
   step buttons apart mid-game. */
.dzc-round span { text-align: center; min-width: 92px; }
.dzc-round b { display: block; font: 800 20px/1 var(--font-display, 'Roboto Slab', serif); }
.dzc-round i { font-style: normal; font-size: 11px; color: var(--ink-2, #6b6459); }
.dzc-play-vp { display: flex; gap: 10px; flex-wrap: wrap; }
.dzc-vp { display: flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: 8px; background: var(--surface-3, #f2ece0); }
.dzc-vp span { font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2, #6b6459); }
.dzc-vp button { width: 28px; height: 28px; border-radius: 6px; cursor: pointer; border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff); font-size: 16px; line-height: 1; }
/* Two digits' worth, because VP reach them and a counter that widens as it
   counts drags the button beside it along. */
.dzc-vp b { min-width: 24px; text-align: center; font-size: 17px; }

/* Play Mode keeps panes too: the counters you consult stay put, the Groups you
   act on scroll beside them. */
.dzc-play-body { display: grid; grid-template-columns: 250px minmax(0, 1fr); gap: 10px; align-items: start; }
.dzc-play-cards {
  display: flex; flex-direction: column; gap: 8px;
  position: sticky; top: 12px; margin-bottom: 0;
}
.dzc-play-main { min-width: 0; }
@media (max-width: 900px) {
  .dzc-play-body { grid-template-columns: 1fr; }
  .dzc-play-cards {
    position: static; flex-direction: row; flex-wrap: wrap; margin-bottom: 12px;
  }
  .dzc-play-cards .dzc-pcard { flex: 1 1 200px; }
}
.dzc-pcard { padding: 9px 10px; border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff); }
.dzc-pcard-k { display: block; font-size: 10px; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-2, #6b6459); }
.dzc-pcard-v { display: block; font: 800 26px/1.1 var(--font-display, 'Roboto Slab', serif); margin: 2px 0 6px; }
/* Both counters reach double figures, and the "/ 4" beside them must not slide
   when they do. */
.dzc-pcard-v b { display: inline-block; min-width: 32px; }
.dzc-pcard-v i { font-style: normal; font-size: 13px; font-weight: 500; color: var(--ink-2, #6b6459); }
.dzc-pcard-act { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.dzc-pcard-act button { padding: 5px 12px; border-radius: 6px; cursor: pointer; border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff); font-size: 13px; }
.dzc-pcard-act input { width: 56px; padding: 4px 6px; border-radius: 5px; border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff); color: var(--ink, #24201c); }

/* ── one row ──
   Name, number and controls on a line, instead of three stacked bands. Wraps
   below about 360px rather than being squeezed: the buttons are 44px on a
   phone and that is not negotiable, so the row is what gives. */
.dzc-pcard--row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px;
  padding: 7px 10px;
}
.dzc-pcard--row .dzc-pcard-k {
  /* Two short lines rather than one long one: "Command Points" set across the
     row is 95px of a 340px phone card spent on a label. */
  display: block; max-width: 70px; line-height: 1.15;
}
.dzc-pcard--row .dzc-pcard-v { margin: 0; font-size: 21px; }
.dzc-pcard--row .dzc-pcard-v b { min-width: 24px; }
.dzc-pcard--row .dzc-pcard-v i { font-size: 12px; }
.dzc-pcard--row .dzc-pcard-act { margin-left: auto; gap: 5px; }

/* Groups, yours against theirs, as one cluster: the word small over the
   number, which is the shape a stat takes everywhere else in this app. */
.dzc-pcard-groups { display: inline-flex; align-items: stretch; gap: 6px; }
.dzc-pg {
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px; line-height: 1;
}
.dzc-pg i {
  font-style: normal; font-size: 9px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--ink-2, #6b6459);
}
.dzc-pg b { font-size: 15px; font-family: var(--font-numeric, inherit); }
.dzc-pg input { width: 46px; text-align: center; }

/* ── a stepper at its stop ──
   It still fires, and pressing it still says why (the float). This is so you
   can see it before you press: CP is generated to your Commander Level at the
   top of every Round, so + is at its ceiling the moment a Round begins, and a
   live-looking button that answers with silence reads as a broken one. */
.dzc-press.is-nil { opacity: .34; }
.dzc-press.is-nil:active { box-shadow: none; }

/* ── the press ──
   Every control on this screen is a bare glyph or a picture, and until now a
   tap produced no acknowledgement at all: the value changed somewhere else on
   the card and you had to go and look for it to know you had hit anything.
   Two answers, and both are needed. This one is the flash under the finger,
   and it is drawn with colour and an inset shadow ONLY -- no padding, no
   border width, no transform that could nudge a neighbour. The other is the
   float, which says what the press meant. */
.dzc-press { transition: background-color .1s, box-shadow .1s, color .1s; }
.dzc-press:active:not([disabled]) {
  background: var(--acc, #1b3a5c) !important;
  border-color: var(--acc, #1b3a5c) !important;
  color: var(--acc-on, #fff) !important;
  box-shadow: inset 0 2px 5px rgba(0,0,0,.25);
}
.dzc-press:active:not([disabled]) svg { color: var(--acc-on, #fff); }
@media (prefers-reduced-motion: reduce) { .dzc-press { transition: none; } }

/* ── the float ──
   A number that rises off what you pressed and fades, the way a game prints
   damage over the thing it hit. It is the only label the icon controls have.

   Fixed and in its own layer, so it cannot lengthen the document, bring a
   scrollbar, or take a tap meant for the button underneath it. */
#dzc-floats {
  position: fixed; inset: 0; z-index: 9600; pointer-events: none; overflow: hidden;
}
.dzc-float {
  position: absolute; transform: translate(-50%, -100%);
  padding: 3px 9px; border-radius: 999px; white-space: nowrap;
  font: 800 13px/1.25 var(--font-display, 'Roboto Slab', serif);
  letter-spacing: .01em;
  background: #2b2118; color: #fff8ec;
  box-shadow: 0 4px 14px rgba(0,0,0,.3);
  animation: dzc-float-up 1.15s cubic-bezier(.16,.84,.44,1) forwards;
}
.dzc-float.is-bad  { background: #a3231a; color: #fff; }
.dzc-float.is-good { background: #1d6b34; color: #fff; }
.dzc-float.is-dead { background: #24201c; color: #ffd7d2; }
/* Nothing happened -- a stepper at its floor or its ceiling. Said quietly and
   said anyway, because a press that changes nothing must not read the same as
   a press that did. */
.dzc-float.is-nil  { background: var(--surface-3, #f2ece0); color: var(--ink, #24201c); font-weight: 600; }
@keyframes dzc-float-up {
  0%   { opacity: 0; transform: translate(calc(-50% + var(--float-shift, 0px)), -70%) scale(.85); }
  14%  { opacity: 1; transform: translate(calc(-50% + var(--float-shift, 0px)), -108%) scale(1.06); }
  30%  { transform: translate(calc(-50% + var(--float-shift, 0px)), -118%) scale(1); }
  100% { opacity: 0; transform: translate(calc(-50% + var(--float-shift, 0px)), -215%) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  /* Still shown, and still gone on its own. Motion is what is removed, not the
     only thing telling you what the button did. */
  .dzc-float { animation: dzc-float-hold 1.15s steps(1) forwards; }
  @keyframes dzc-float-hold {
    0%, 85% { opacity: 1; transform: translate(-50%, -115%); }
    100%    { opacity: 0; transform: translate(-50%, -115%); }
  }
}

.dzc-play-group { border: 1px solid var(--line, #d8d0c0); padding: 8px 9px; margin-bottom: 7px; background: var(--surface-2, #fff); }
.dzc-play-group.is-done { opacity: .58; }
.dzc-play-group.is-dead { opacity: .4; }
.dzc-play-group > header { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 6px; }
.dzc-act { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.dzc-act b { font: 700 14px/1.2 var(--font-ui, 'Jost', sans-serif); }
.dzc-play-tag { display: inline-flex; align-items: center; gap: 4px; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; padding: 2px 7px; border-radius: 3px; background: #dbe9f5; color: #16466e; }
/* A Squad, with the model behind its own numbers.
   Which is how you find one on a screen of eleven: you are looking for the
   shape you just picked up off the table, not for its name. The art is inert
   and decorative -- `contain`, so it is never stretched, and pinned right so
   it sits under the model count rather than under the name.

   IN COLOUR, AND NOT FADED OUT. Jet, 2026-08-17: "have those vague outlines
   behind not fading, and give them color." The first pass drained it to grey
   and ran a gradient mask across it, which left half a ghost of a silhouette
   -- enough to notice, not enough to recognise, which is the one thing it is
   there to do. Whole picture, own colours, one flat opacity. */
.dzc-play-squad {
  position: relative; isolation: isolate;
  padding: 6px 0; border-top: 1px solid var(--line, #eee5d6);
}
.dzc-play-art {
  position: absolute; z-index: 0; pointer-events: none; user-select: none;
  /* HEIGHT, not top-and-bottom. An <img> is a replaced element: with `height:
     auto` it takes its INTRINSIC height and the `bottom` offset is thrown away
     as over-constrained, so a pair of offsets sized it to the photograph
     rather than to the Squad. */
  top: 4px; right: 0; height: calc(100% - 8px); width: min(46%, 210px);
  object-fit: contain; object-position: right center;
  opacity: .2;
}
[data-theme="dark"] .dzc-play-art { opacity: .26; }
/* Everything the Squad prints sits above the watermark. One wrapper rather
   than a z-index on each child: a weapon card and a rule chip both open
   popovers, and stacking contexts fought over which was on top. */
.dzc-play-sq { position: relative; z-index: 1; }
.dzc-play-sq-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* What this Squad IS, tappable. Same chips the reference and the builder use,
   so a rule reads and opens identically wherever you meet it. */
.dzc-play-rules {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 6px;
}
/* Which Variant a restricted rule belongs to (3.2.2). A word, not a heading:
   the chips after it are the sentence. */
.dzc-play-rule-of {
  font: 600 10px/1 var(--font-ui, 'Jost', sans-serif);
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2, #6b6459);
  margin-left: 4px;
}
.dzc-play-name { font-weight: 600; font-size: 13.5px; overflow-wrap: anywhere; }
.dzc-play-alive { margin-left: auto; font-size: 12px; font-weight: 700; }
.dzc-play-models { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 5px; }
/* The guns this Squad fires, at a table. Scrolls sideways in its own box like
   every other weapon table; the page never does. */
/* No second scroller, and no min-width.
   .dzc-wpn is already `display: block; overflow-x: auto`. It scrolls itself,
   the way it does in the reference and the builder. Wrapping it in another
   scroller and then setting min-width: 560px on the table made the SCROLL
   CONTAINER 560px wide, so on a 320px phone Play Mode ran 260px off the right
   of the page instead of scrolling inside its card. min-width: 0 on the chain
   above it, because a flex item's default min-width is its content and that is
   what stops a scroller from ever being narrower than its table. */
/* A Behemoth's condition. Degraded and Crippled change what it may do that
   activation, so they read as a state rather than as a number to work out. */
.dzc-play-cond {
  margin: 4px 0 0; font-size: 11.5px; font-weight: 600;
  color: var(--ink-2, #6b6459);
}
.dzc-play-cond.is-degraded { color: #a86a12; }
.dzc-play-cond.is-crippled { color: #a3231a; }
/* Power tokens. Only a Behemoth has any, and what you look at mid-Round is
   "can it go again". So the dots carry it and the number confirms it. Same
   controls as the damage stepper beside it, because it is the same gesture. */
/* RM aboard a Genitor, in the same row shape as Power. No dot track: Power
   tops out at 10 and reads as pips, RM goes to 12 and would be confetti. */
.dzc-play-rm svg { flex: 0 0 auto; color: var(--acc-text, var(--acc, #1b3a5c)); }
.dzc-play-pt, .dzc-play-rm { display: flex; align-items: center; gap: 6px; margin-top: 5px; flex-wrap: wrap; }
.dzc-play-pt button, .dzc-play-rm button {
  min-width: 26px; min-height: 26px; border-radius: 5px; cursor: pointer;
  border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff);
  font-size: 15px; line-height: 1;
}
/* PT goes to 10 and RM to 12: two digits' worth, held open. */
.dzc-play-pt b, .dzc-play-rm b { font-family: var(--font-numeric, inherit); font-size: 13px; min-width: 17px; text-align: right; }
.dzc-play-pt i, .dzc-play-rm i { font-style: normal; font-size: 11px; color: var(--ink-2, #6b6459); }
.dzc-pt-track { display: flex; gap: 3px; align-items: center; }
.dzc-pt-dot {
  width: 9px; height: 9px; border-radius: 50%;
  border: 1px solid var(--line, #d8d0c0); background: transparent;
}
.dzc-pt-dot.is-on { background: var(--acc, #1b3a5c); border-color: var(--acc, #1b3a5c); }
.dzc-play-wpn {
  margin-top: 6px; min-width: 0;
  display: grid; gap: 5px; grid-template-columns: repeat(auto-fill, minmax(min(232px, 100%), 1fr));
}
.dzc-play-squad, .dzc-play-models { min-width: 0; }
.dzc-model { display: flex; align-items: center; gap: 4px; padding: 4px 7px; border-radius: 6px; background: var(--surface-3, #f2ece0); }
.dzc-model.is-dead { opacity: .45; text-decoration: line-through; }
.dzc-model button { width: 26px; height: 26px; border-radius: 5px; cursor: pointer; border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff); font-size: 15px; line-height: 1; }
/* A Behemoth has 22 DP, so the well has to hold two digits from the start or
   the stepper widens the first time one takes a hit. */
.dzc-model b { min-width: 20px; text-align: center; font-size: 14px; }
.dzc-model i { font-style: normal; font-size: 10px; color: var(--ink-2, #6b6459); }
.dzc-statuses { display: inline-flex; gap: 3px; margin-left: 3px; }
/* THE PRINTED TOKEN, AT ONE SIZE, ALWAYS.
   It used to be a letter -- C, S, J, O -- that swelled into the whole word
   when you set it, which took the button from 22px to about 110px and shoved
   the model count on to its own line. Pressing a control must never move the
   things beside it, and this was the one place in the app that did.
   So: the picture off page 51 of the rulebook, the shape actually sitting
   beside the models, at a fixed box in both states. Set is full colour on the
   faction accent; unset is the same token drained and quiet. Nothing resizes,
   so nothing can move. The name rises off it on tap (.dzc-float). */
.dzc-st {
  width: 30px !important; height: 30px !important; padding: 0 !important;
  display: inline-grid !important; place-items: center;
  font-size: 11px !important; font-weight: 700; color: var(--ink-2, #6b6459);
  overflow: visible;
}
.dzc-st img {
  width: 74%; height: 74%; object-fit: contain; display: block;
  /* Quiet, not invisible. An unset token at low opacity was a smudge; drained
     of colour at full strength it still reads as a shape you can recognise. */
  filter: grayscale(1) contrast(.55) brightness(1.12); opacity: .62;
  transition: filter .12s, opacity .12s;
}
/* A Behemoth cannot take one (1.2). Without this the refusal looked exactly
   like every other button on the row and the only thing saying otherwise was
   a click that did nothing. */
.dzc-st[disabled] {
  opacity: .3; cursor: not-allowed; border-style: dashed;
}
.dzc-st.is-on {
  background: var(--acc, #1b3a5c) !important;
  border-color: var(--acc, #1b3a5c) !important;
  color: var(--acc-on, #fff);
}
.dzc-st.is-on img { filter: none; opacity: 1; }
/* All four Status Tokens are printed as a black disc with white line art, so
   the drained version of one on a dark card is dark on dark. Lifted rather
   than inverted: a white disc would be a different token to recognise, and
   recognising it is the whole reason it is a picture. Set, it sits on the
   faction fill and needs nothing. */
[data-theme="dark"] .dzc-st img { filter: grayscale(1) brightness(2.1) contrast(.75); opacity: .72; }
@media (prefers-reduced-motion: reduce) { .dzc-st img { transition: none; } }

/* ── collection ── */
.dzc-coll-list { display: grid; gap: 6px; }
.dzc-coll-row { display: flex; align-items: center; gap: 10px; padding: 7px 9px; border: 1px solid var(--line, #d8d0c0); border-radius: 6px; background: var(--surface-2, #fff); }
.dzc-coll-row.is-owned { border-left: 3px solid var(--acc); }
.dzc-coll-row img, .dzc-coll-row .dzc-pick-noart { flex: 0 0 44px; width: 44px; height: 44px; object-fit: contain; }
.dzc-coll-body { flex: 1 1 auto; min-width: 0; }
.dzc-coll-name { display: block; font-weight: 600; font-size: 13.5px; overflow-wrap: anywhere; }
.dzc-coll-meta { display: block; font-size: 11px; color: var(--ink-2, #6b6459); }
/* Advisory, never blocking. Owning too few models is not against the rules. */
.dzc-short { margin: 0 0 12px; padding: 9px 12px; border-radius: 6px; font-size: 12.5px; background: var(--surface-3, #f2ece0); }
.dzc-short b { display: block; margin-bottom: 3px; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2, #6b6459); }

/* Firing arcs are drawn as 90° wedges (6.1.2). The letters stay beside the
   picture: the icon is faster to read, the text is what the card says. */
.dzc-arc { display: inline-block; vertical-align: middle; line-height: 0; }
.dzc-arc-cell { white-space: nowrap; }
.dzc-arc-cell span { margin-left: 4px; vertical-align: middle; }
.dzc-army-btns { display: inline-flex; gap: 2px; flex: 0 0 auto; }
.dzc-list-btns { display: inline-flex; gap: 8px; flex-wrap: wrap; }

/* ── New Army dialog ──
   Faction as a grid of accented buttons; game size as cards that state what
   each one buys, because "Clash" alone does not tell you the Group cap. */
.dzc-faction-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.dzc-faction-btn {
  display: flex; align-items: center; gap: 7px; padding: 9px 10px; cursor: pointer;
  border: 1px solid var(--line, #d8d0c0); border-radius: 6px; background: var(--surface-2, #fff);
  font: 600 12.5px/1 var(--font-ui, 'Jost', sans-serif); color: var(--ink, #24201c);
  transition: border-color .15s cubic-bezier(.33,0,.67,1), background .15s cubic-bezier(.33,0,.67,1);
}
.dzc-faction-btn:hover { border-color: var(--acc); }
.dzc-faction-btn.selected { border-color: var(--acc); box-shadow: inset 3px 0 0 var(--acc); }
/* The real faction art, all six. The accent square behind it is the fallback
   if a file ever goes missing; onerror removes the image rather than showing a
   broken one. */
.dzc-faction-icon { width: 20px; height: 20px; object-fit: contain; flex: 0 0 auto; }
.dzc-faction-icon + .dzc-faction-dot { display: none; }
.dzc-faction-dot { width: 11px; height: 11px; border-radius: 2px; background: var(--acc); flex: 0 0 auto; }
.dzc-points-row { margin-top: 8px; }

/* The quick-reference tile is a real link, because it goes to a real page
   rather than a view. So it can be opened in a new tab, which is what you
   want from something you are about to print. It has to stop looking like a
   link to sit beside the three tiles that are not. */
a.tool-card { display: block; text-decoration: none; color: inherit; }

/* ── Dropzone wordmark ──
   TTCombat's own DZC logo, two cuts: all-white for the navy topbar, and the
   black/green cut for the light landing page. Dark mode swaps the landing one
   to white, because the black cut disappears against a dark background. */
/* THE WORDMARK IS NEVER SQUASHED.
   `width: auto` does not survive being a flex item: the topbar runs out of
   room, the image is shrinkable by default, and the browser narrows it while
   the fixed height holds -- so a 1600x230 wordmark was drawn at 27x20 in the
   builder at 375px, which is 81% off its own aspect ratio. Measured across
   four widths and five routes on 2026-08-17; it was wrong on every phone-width
   screen in the app and nowhere else.
   `flex: none` stops the shrink and `object-fit: contain` means that if
   anything ever does constrain the box again, the picture letterboxes inside
   it instead of distorting. */
.topbar-logo {
  height: 26px; width: auto; display: block;
  flex: none; object-fit: contain;
}
.landing-logo-img { max-width: min(440px, 78vw); width: 100%; height: auto; display: block; margin: 0 auto; }
[data-theme="dark"] #landing-logo { content: url("../assets/logos/dzc_logo_white.webp"); }
@media (max-width: 640px) { .topbar-logo { height: 20px; } }
/* And below the phone breakpoint it is cut. Jet, 2026-08-17: "feel free to cut
   the DZC logo." Unsquashed it wants 139px, and the bar it shares is already
   carrying the disclaimer, the way back, the screen you are on and two
   buttons; on a 320px phone that is nearly half the width spent on a picture
   of the game's name. Nothing is lost with it -- the way back to the landing
   screen is the chevron that is already there on every screen but that one,
   and the landing screen has the wordmark at size in its own hero. */
@media (max-width: 767px) { .topbar-brand { display: none; } }

/* css/app.css hides .topbar-context entirely below 768px. That is Dropfleet-era
   and it was right there, because a phone got the separate mobile/ build with
   its own chrome. That build is deleted and this is one responsive app, so the
   rule left a phone with no back chevron on any view and no Reset game at all
   in Play Mode. On the case CLAUDE.md §4 says has to work, because it is the
   one used at a table.

   The chevron and the buttons stay; the label goes, because it is the label
   that has no room beside four icon buttons. */
@media (max-width: 768px) {
  .topbar-context { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
  .topbar-back-label { display: none; }
}

/* ── A menu hanging off a card ──
   At <body> level and fixed, so opening it moves nothing on the page. Square,
   like every other panel; the items inside are rows, not buttons you aim at
   individually, so they have no radius either. */
.dzc-pop-menu {
  z-index: 9200; min-width: 168px; padding: 4px;
  border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .18);
}
.dzc-pop-menu button {
  display: flex; align-items: center; gap: 9px; width: 100%; padding: 8px 10px;
  border: 0; border-radius: 0; background: none; cursor: pointer; text-align: left;
  font: 500 13px/1.2 var(--font-ui, 'Jost', sans-serif); color: var(--ink, #24201c);
}
.dzc-pop-menu button:hover, .dzc-pop-menu button:focus-visible {
  background: var(--surface-3, #f2ece0);
}
.dzc-pop-menu button.is-danger { color: var(--danger, #b3261e); }

/* ── Drag a Group to reorder it ──
   Six dots. It is the one glyph in the app that means "hold this", and it is
   the only drag target. Dragging the card itself would fight the tap that
   opens it, which is the commonest thing you do to one.

   THE LEADING EDGE, full height, and visible without being hovered.

   It was a 16px icon at 35% opacity in the top-right corner, and two players
   on a PC concluded from that that the app had no drag and drop at all.
   Grotwurks, 2026-08-09: "There is no drag function on PC". Then, after being
   told where to look, "Yea, I see it now but it is very hard to see", "OHHHHH
   there they are.... at 200% zoom I can see them", and "I had my friend try
   drag and drop and he didnt see the section till I pointed it out". Baxter,
   who could use it, agreed: "It's not intuitive enough if we're having trouble
   with it."

   Three separate faults, and only the first is the obvious one.

   - 35% of --ink-2 measures 1.60:1 against the card. A control has to clear
     3:1 to be seen at all (WCAG 1.4.11), so it was at half the floor. It is
     .8 now: 3.36:1 on the selected card's tinted ground and 3.67:1 on dark,
     full on hover or focus. Resting has to be the visible state, because a
     phone has no hover to reveal anything with.
   - The corner was the wrong CORNER. The Squad grip below is at the leading
     edge, for reasons its own comment argues at length, so the two handles for
     the same gesture sat at opposite ends of the screen, Baxter, on the same
     day: "The six squares are on the opposite side on the full group page on
     phones." One position, learned once. It also gives the cost digits their
     24px back: .dzc-bb-head no longer has to reserve a gap for dots.
   - A 16px target in a corner is a target you have to aim at. Full height of
     the card by 26px wide is one you land on. */
.dzc-bb { position: relative; padding-left: 32px; }
.dzc-bb-grip {
  position: absolute; left: 0; top: 0; bottom: 0; width: 26px;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: grab; touch-action: none; opacity: .8; color: var(--ink-2, #6b6459);
  transition: opacity .12s, background .12s;
}
.dzc-bb:hover .dzc-bb-grip, .dzc-bb-grip:hover { opacity: 1; background: var(--surface-3, #f2ece0); }
/* Both grips take focus now, the arrows do the move without a pointer, so
   both need to show they have it. Inset, because a grip sits hard against the
   edge of the thing it moves and an outset ring would be clipped by the
   neighbour or hang off the card. */
.dzc-bb-grip:focus-visible, .dzc-sq-grip:focus-visible {
  opacity: 1; outline: 2px solid var(--acc); outline-offset: -2px;
}
/* No shadow and no lift: the card must not move except with the finger. */
.dzc-bb.is-dragging { opacity: .9; position: relative; z-index: 50; cursor: grabbing; }
.dzc-bb.is-before { box-shadow: inset 0 3px 0 0 var(--acc); }
.dzc-bb.is-after { box-shadow: inset 0 -3px 0 0 var(--acc); }

/* A Squad with one legal size is a multiplier, not a control. */
.dzc-sq-fixed {
  font: 600 13px/1 var(--font-numeric, 'Outfit', sans-serif); color: var(--ink-2, #6b6459);
}

/* Dragging a Squad into a Transport. Same dot grip as a Group's, in the title
   line where the Squad's name is, because the name is the thing you are
   moving. touch-action: none or the phone scrolls the pane instead. */
/* Jet's grab icon, not a background gradient pretending to be one. */
/* THE LEADING EDGE, before everything. It sat between the thumbnail and the
   name, which is the one place a handle should never be: neither the start of
   the row nor the end of it, just interrupting the middle of the thing it
   moves. Every list that does this well -- Material, Notion, Jira, Figma --
   puts the handle first, because it grabs the WHOLE row and a row is read
   left to right, so its handle belongs where reading starts. (iOS puts it on
   the right, but only in an edit mode where reordering is the sole thing the
   list does; here it is one action among six.) */
/* BIGGER. Jet, 2026-08-07: "grip and touch targets must be bigger." 22px
   wide is a 22px target however tall it is -- a finger pad is about 45px
   across, and the misses are sideways as often as vertical. It is 34 wide and
   the full height of the row now, which on a Squad with a photo is 60px. */
/* AND VISIBLE. It was 28% of --ink-2 -- 1.49:1 against the card, which is to
   say not drawn at all until you hovered it. The size was already right and
   nobody could find it anyway: see the Group grip above for the thread. .8
   measures 3.76:1 on paper and 4.07:1 on dark, full on hover or focus, and
   resting is the state that has to work because a phone never hovers. */
.dzc-sq-grip {
  flex: 0 0 auto; align-self: stretch; display: inline-flex; align-items: center;
  justify-content: center; min-width: 34px; min-height: 44px; margin-left: -6px;
  cursor: grab; touch-action: none; opacity: .8; color: var(--ink-2, #6b6459);
  transition: opacity .12s, background .12s;
}
.dzc-squad:hover > .dzc-sq-main > .dzc-sq-grip, .dzc-sq-grip:hover {
  opacity: 1; background: var(--surface-3, #f2ece0);
}
/* Nothing moves. The card stays exactly where it is and a chip follows the
   finger instead, which is the only thing on screen that travels. */
.dzc-squad.is-sq-dragging { opacity: .45; cursor: grabbing; }
/* A Squad taken down to zero models stays -- it is not deleted, only empty
   (Jet, 2026-08-09: "make the entire card greyed out with 0 models but not
   deleted"). First cut dimmed the WHOLE card, opacity being an easy switch --
   but opacity multiplies through everything under it, stepper and Variant
   dots included, so the one control you actually need to press dimmed along
   with the art. Jet, same day: "obviously leave the option to add
   selections back... not prevent them from picking something... that goes
   double for any of those units that are max size 1" -- exactly the dots.
   So only the parts that are ABOUT a model you do not have dim: the art, the
   name, the meta line, the stat grid, the cost. .dzc-sq-ctl (stepper,
   Transport picker, Remove) and .dzc-vblock-n (the Variant dots/stepper) are
   never touched -- full contrast, because they are the way out. */
.dzc-squad.is-empty .dzc-sq-art { filter: grayscale(1) opacity(.6); }
.dzc-squad.is-empty .dzc-sq-name,
.dzc-squad.is-empty .dzc-sq-meta,
.dzc-squad.is-empty .dzc-sq-cap,
.dzc-squad.is-empty .dzc-sq-stats,
.dzc-squad.is-empty .dzc-sq-cost,
.dzc-squad.is-empty .dzc-vblock-head > b,
.dzc-squad.is-empty .dzc-vblock-head > i { opacity: .55; }
.dzc-drag-ghost {
  position: fixed; z-index: 900; pointer-events: none;
  transform: translate(12px, 10px);
  padding: 4px 9px; font: 600 12px/1 var(--font-ui, 'Jost', system-ui, sans-serif);
  background: var(--acc, #1b3a5c); color: var(--acc-on, #fff);
}
/* Every Transport in this Group that could legally take it, lit at once, so
   the answer to "where can this go" is on screen before you have moved. Asked
   for once at pointerdown from the same boardOptions the chooser lists. A
   drop can never land somewhere the rules refuse. */
.dzc-squad.is-drop > .dzc-sq-main { outline: 2px dashed var(--acc, #1b3a5c); outline-offset: 3px; }
.dzc-squad.is-drop-on > .dzc-sq-main {
  outline: 3px solid var(--acc, #1b3a5c); outline-offset: 3px;
  background: color-mix(in srgb, var(--acc, #1b3a5c) 12%, transparent);
}
/* And the Group itself is the target that takes a Squad back OFF its
   Transport. The other half of the gesture, and the only way to undo it
   without opening the chooser. */
.dzc-group-card.is-dropping.can-walk { outline: 2px dashed var(--line, #d8d0c0); outline-offset: 2px; }
.dzc-group-card.is-walk-on { outline: 3px solid var(--acc, #1b3a5c); outline-offset: 2px; }
/* And every OTHER Group in the list beside it, which is where a Squad goes
   when it is leaving this one. Same two states as a Transport target, because
   it is the same gesture landing on a different kind of thing. */
.dzc-bb.is-drop { outline: 2px dashed var(--acc, #1b3a5c); outline-offset: 2px; }
.dzc-bb.is-drop-on {
  outline: 3px solid var(--acc, #1b3a5c); outline-offset: 2px;
  background: color-mix(in srgb, var(--acc, #1b3a5c) 12%, transparent);
}

/* ── What an army is for ──
   On the card it is one quiet line under the name. In the builder it is
   editable where it is read, like the name above it. And when it is empty it
   shows its prompt in placeholder ink rather than an empty box with a caption
   under it. :empty is exact here: contenteditable leaves the element with no
   child nodes until something is typed. */
.dzc-army-desc {
  margin: 1px 0 0; font-size: 11.5px; line-height: 1.35; color: var(--ink-2, #6b6459);
  overflow-wrap: anywhere;
}
/* Notes, at the foot of the army. Ruled off, because it is no longer next to
   the name that identified it and needs to read as its own thing rather than
   as a stray line after the last Group. */
.dzc-b-desc {
  margin: 10px 0 0; padding: 4px 0 0;
  font-size: 13px; line-height: 1.45; color: var(--ink-2, #6b6459);
  overflow-wrap: anywhere; cursor: text; min-height: 1.45em;
}
.dzc-b-desc:empty::before { content: attr(data-empty); opacity: .55; }
.dzc-b-desc:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }

/* ── Share, three targets ──
   A row per target: what it is, and what it is for. Square, because it reads
   as a card and not as a button, whatever tag it happens to be. */
.dzc-share-opt {
  display: flex; align-items: center; gap: 10px; width: 100%; margin-bottom: 6px;
  padding: 11px 13px; cursor: pointer; text-align: left;
  border: 1px solid var(--line, #d8d0c0); background: var(--surface-2, #fff);
  color: var(--ink, #24201c);
  transition: border-color .12s, background .12s;
}
.dzc-share-opt:hover, .dzc-share-opt:focus-visible { border-color: var(--acc, #1b3a5c); }
.dzc-share-opt b { font: 600 14px/1.2 var(--font-ui, 'Jost', sans-serif); }
.dzc-share-opt span { flex: 1 1 auto; font-size: 12px; color: var(--ink-2, #6b6459); }
.dzc-share-opt svg { flex: 0 0 auto; color: var(--ink-2, #6b6459); }

/* ── Sharp cards ──
   Every card surface is square. Buttons, chips and inputs keep their radius.
   A control can be soft, a panel should not be. One rule in one place so the
   next card someone adds does not have to remember.

   IT HAS TO BE LAST IN THE FILE. It used to sit halfway up, above the rules
   that declare .dzc-pick, .dzc-rail-card and .dzc-cmdr-opt at 8px. So the
   picker cards and the whole rail were still rounded, weeks after the rule was
   made, and nothing said so. Same specificity means the later rule wins.
   scripts/test-house-rules.mjs now reads the cascade and fails if any listed
   surface's LAST word is not zero, which is how that was found. */
.dzc-pick,
.dzc-card,
.dzc-army-card,
.dzc-army-new,
.dzc-rail-card,
.dzc-group-card,
.dzc-issues,
.dzc-cmdr-opt,
.dzc-cmdr-add,
.dzc-pcard,
.dzc-play-group,
.dzc-stats .dzc-stat,
/* Panels that were missed the first time and are still panels: the rule
   popover, the toast, a Collection row, the
   shortfall note and the category meters. */
.dzc-pop,
.dzc-toast,
.dzc-coll-row,
.dzc-short,
.dzc-ratio,
.dzc-share-opt,
/* And the three that live in css/app.css, which is Dropfleet-era and still
   rounds everything: the landing tiles, and both grids in the New Army dialog.
   They are the first surfaces anyone sees, so a rounded one there reads as the
   app's shape rather than as an oversight. */
.dzc-faction-btn,
.tool-card,
.game-size-option,
.modal-panel { border-radius: 0; }

/* ── Quick play ──
   Two starter-box lists as cards. Same shape as an army card cut down to what
   tells them apart: whose it is, what it is called, and what you are getting. */
.dzc-quick-grid { display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.dzc-quick-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 5px;
  padding: 10px 12px; cursor: pointer; text-align: left;
  border: 1px solid var(--line, #d8d0c0); border-left: 4px solid var(--acc);
  border-radius: 0; background: var(--surface-2, #fff); color: inherit;
  transition: box-shadow .12s, border-color .12s;
}
.dzc-quick-card:hover, .dzc-quick-card:focus-visible {
  box-shadow: inset 0 0 0 1px var(--acc);
}
.dzc-quick-fac { display: inline-flex; align-items: center; gap: 6px; }
.dzc-quick-fac img { width: 20px; height: 20px; object-fit: contain; }
.dzc-quick-fac b {
  font: 600 11px/1.2 var(--font-ui, 'Jost', sans-serif);
  letter-spacing: .04em; text-transform: uppercase; color: var(--ink-2, #6b6459);
}
.dzc-quick-name { font: 700 16px/1.2 var(--font-display, 'Roboto Slab', serif); }
.dzc-quick-meta {
  font: 500 12px/1.3 var(--font-numeric, 'Outfit', sans-serif); color: var(--ink-2, #6b6459);
}


/* ── a phone can hit the control, part two ────────────────────────────────
   Everything below was measured in a real Chrome at 390x844, walking all 41
   screens and reporting every interactive box under 44px. mobile-fixes.css
   got the first pass and named the controls the complaint was about; this is
   what a sweep found afterwards, and it is mostly Play Mode, which is the one
   screen used entirely with a finger while standing at a table.

   It lives at the end of dzc.css rather than in mobile-fixes.css because
   mobile-fixes.css is loaded FIRST, so on equal specificity dzc.css wins and
   a phone override placed there loses silently. The status buttons carry
   !important for the same reason, one level down: their size is !important
   here already.

   The exemptions from the first pass still stand and are not repeated: rule
   chips and footer links stay small on purpose, both for reasons written
   where they are set. Two more join them: .dzc-wc-e is an underlined number
   inside a weapon card, inline in a line of stats, and .dzc-rule--inline is
   the same idea in a sentence. */
@media (max-width: 768px) {
  /* Play Mode. Round, VP, Command Points, Pass tokens, and the four Status
     Tokens, which were 22px square. (Initiative was in this list until its
     card was removed: it rolled a D6 the app could not see, beside the one on
     the table.) */
  .dzc-round button,
  .dzc-vp button,
  .dzc-pcard-act button,
  .dzc-play-pt button,
  .dzc-play-rm button,
  /* The damage stepper. 26px square, and it is pressed more than anything
     else on this screen: it is how a model loses a DP. */
  .dzc-model button,
  /* And the variant stepper in the builder, which is how a Squad gets its
     models. Two classes deep, because the desktop rule that sets it to 34x32
     is .dzc-vblock-n .dzc-stepper button and a single class loses to it. */
  .dzc-vblock-n .dzc-stepper button,
  .dzc-v-step button {
    min-width: 44px;
    min-height: 44px;
  }
  /* TWO STEPPERS TO A ROW ON A PHONE.
     At 44px a side the buttons are right, and at 320px two of them plus the
     number came to 149px -- six pixels too wide to pair up, so a six-model
     Squad was six rows of stepper, 312px of screen for six numbers. The
     buttons keep their size; the padding, the gaps and the well give way, and
     the row becomes a grid so the pair share the width evenly instead of one
     of them deciding it. */
  .dzc-play-models {
    display: grid; gap: 6px;
    /* 1fr, not a fixed ceiling. auto-fit counts its repetitions against the
       track's MAX when that max is definite, so minmax(118px, 152px) at 320
       fits one column of 152 rather than two of 131 -- the opposite of what
       this rule is for. The cap belongs on the stepper instead
       (.dzc-model max-width), which leaves the spare width of a wide track
       empty rather than blowing one stepper out to 236px of grey around a
       two-character number. */
    grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
  }
  .dzc-model { gap: 2px; padding: 4px 3px; justify-content: center; max-width: 152px; }
  .dzc-model b { min-width: 18px; }
  /* Two VP counters were one row and became two the moment their buttons went
     from 28 to 44, which is 60px of a phone screen spent before you can see
     any of your army. They still fit side by side, on the padding and the gaps
     rather than on the size of the thing you press. */
  .dzc-play-vp { gap: 6px; }
  .dzc-vp { gap: 2px; padding: 0 4px; }
  .dzc-vp b { min-width: 22px; }
  /* Same box set or unset -- there is no "is-on" size any more, which is the
     point of it being a picture rather than a word that grows. */
  .dzc-st {
    width: 44px !important;
    height: 44px !important;
    font-size: 13px !important;
  }
  .dzc-statuses { gap: 5px; }

  /* The primary action in the picker was 36px, and the rail is how you read
     your points on a phone. */
  .dzc-pick-add,
  .dzc-rail-peek,
  .dzc-share-opt,
  .dzc-view-toggle,
  .dzc-shape-chip,
  .dzc-sq-btn,
  /* Two the first pass did not reach, found by sweeping every interactive
     element on every route and modal at 390 rather than by reading the rules
     and deciding they looked covered.

     A paid weapon upgrade at 62x32. It is a button that spends points, it is
     the only control on a weapon card, and it is pinned to a corner where a
     thumb arrives at an angle. The card reserves 66px beside the weapon name
     for it, so growing it downward costs nothing.

     And the opponent's Group count in Play Mode at 56x25, which is typed into
     while standing at a table -- the one screen where every other control was
     already taken to 44. */
  .dzc-buy,
  .dzc-pcard-act input[type="number"],
  #sync-token-input {
    min-height: 44px;
  }
  .dzc-view-toggle,
  .dzc-shape-chip { min-width: 44px; }

  /* A grip is dragged, so its width is the target as much as its height. */
  .dzc-sq-grip, .dzc-bb-grip { min-width: 44px; }

  /* A checkbox renders at its font size, 13px, and no padding will grow it.
     The activation box in Play Mode is the single most-pressed control there:
     it is how a Group is marked done. */
  .dzc-act input[type="checkbox"],
  .dzc-play-group input[type="checkbox"],
  .dzc-set-toggle input[type="checkbox"],
  .pp-opt input[type="checkbox"] {
    width: 26px; height: 26px;
  }
  /* The label is the target the box sits in, so that is what has to clear 44,
     and a bare checkbox never will. */
  .dzc-act, .dzc-set-toggle, .pp-opt {
    min-height: 44px; display: inline-flex; align-items: center;
  }
  .dzc-set-toggle { display: flex; justify-content: space-between; }

  /* Chips are as wide as their word, and "All" is a short one. */
  .dzc-chip, .dzc-chip--sm { min-width: 44px; }
  /* Settings: the theme pair and the picker's Clear. */
  .dzc-seg button, .dzc-pick-bar button { min-height: 44px; }
}
