/* ============================================================
   Ink Fusion v28 — page-specific blocks
   (comparison tables, verdict, checklists, calculators, wizard, tiers)
   ============================================================ */

/* ---- verdict banner (vs pages) ---- */
.verdict { border: 2.5px solid var(--ink); background: var(--ink); color: var(--paper); padding: 26px 30px; box-shadow: 8px 8px 0 var(--cyan); }
.verdict p { font-size: 16px; line-height: 1.6; margin: 0; }
.verdict strong { color: var(--yel); }

/* ---- comparison table ---- */
.cmp { width: 100%; border-collapse: collapse; border: 2.5px solid var(--ink); font-size: 14.5px; }
.cmp th, .cmp td { border: 2px solid var(--ink); padding: 12px 14px; text-align: left; vertical-align: top; }
.cmp thead th { font-family: var(--f-display); font-weight: 800; text-transform: uppercase; letter-spacing: -0.01em; font-size: 16px; }
.cmp thead th:nth-child(2) { background: var(--cyan); }
.cmp thead th:nth-child(3) { background: var(--mag-ground); color: var(--paper); }
.cmp thead th:nth-child(4) { background: var(--yel); }
.cmp thead th:nth-child(5) { background: var(--orange); color: var(--ink); }
.cmp tbody th { font-family: var(--f-mono); font-weight: 700; font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; background: var(--paper-2); white-space: nowrap; }
.cmp tbody td:nth-child(2) { background: rgba(0,167,225,0.12); }
.cmp-wrap { overflow-x: auto; }

/* ---- choose-if checklist ---- */
.vs-check { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
.vs-check .col { border: 2.5px solid var(--ink); padding: 26px; box-shadow: 6px 6px 0 var(--ink); }
.vs-check .col.dtf { background: var(--cyan); }
.vs-check .col.alt { background: var(--paper-2); }
.vs-check h3, .vs-check h2 { font-family: var(--f-display); font-weight: 800; text-transform: uppercase; font-size: 22px; margin-bottom: 14px; letter-spacing: -0.01em; }
.vs-check ul { list-style: none; }
.vs-check li { padding: 7px 0 7px 26px; position: relative; font-size: 14.5px; border-top: 1px solid rgba(21,19,16,0.15); }
.vs-check li:first-child { border-top: 0; }
.vs-check li::before { content: "→"; position: absolute; left: 0; font-weight: 700; }
@media (max-width: 760px) { .vs-check { grid-template-columns: 1fr; } }

/* ---- Custom DTF calculator ---- */
.calc { display: grid; grid-template-columns: 1.3fr 0.7fr; gap: 30px; align-items: start; }
@media (max-width: 900px) { .calc { grid-template-columns: 1fr; } }
/* ── Garment switcher ──────────────────────────────────────────────────────
   Sits above the size grid because garment is the question a customer can
   answer instantly, and answering it makes every size below legible. It costs
   one row and changes no price. */
.gswitch-wrap { margin-bottom: 20px; }
.gswitch-label { display: block; font-family: var(--f-mono); font-size: 11px; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 8px; }
.gswitch { display: flex; flex-wrap: wrap; gap: 8px; }
.gopt {
  flex: 1 1 auto; min-width: 104px; padding: 10px 12px; cursor: pointer;
  border: 2px solid var(--ink); background: var(--paper); color: var(--ink);
  font-family: var(--f-mono); font-size: 11px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; text-align: center;
  transition: background var(--dur-state) var(--ease-state), box-shadow var(--dur-state) var(--ease-state);
}
@media (pointer: fine) { .gopt:hover { box-shadow: 3px 3px 0 var(--ink); } }
.gopt:active { box-shadow: 1px 1px 0 var(--ink); }
.gopt:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
/* Selected inverts to ink-on-paper rather than taking a colour. An inversion
   survives greyscale and colour blindness on its own, which a cyan fill does
   not — the same reason the preset tile carries an inset bar as well as its
   fill. The pattern already exists on .dtf-ai__preset.on. */
.gopt.on { background: var(--ink); color: var(--paper); box-shadow: 3px 3px 0 var(--cyan); }
@media (max-width: 480px) {
  /* Five across cannot hold "LONG SLEEVE" on a phone; two columns can. */
  .gswitch { display: grid; grid-template-columns: repeat(2, 1fr); }
  .gopt { min-width: 0; }
}

/* ── Garment preview ───────────────────────────────────────────────────────
   Shows the chosen size WHERE IT ACTUALLY GOES: Mini on a sleeve, Square on the
   back. The point is to answer "how big is 21 x 29.7cm" in a form a person can
   read, which a number cannot do.

   The box is RESERVED. aspect-ratio holds the space before any image loads and
   object-fit: contain means a garment of a different shape letterboxes inside it
   rather than resizing it — the tiles below must never jump while someone is
   choosing. Nothing here may set a background: the plates are keyed to
   transparency, so a white fill would put a white box back behind them. */
.gpreview { margin: 0 0 20px; border: 2.5px solid var(--ink); background: var(--paper-2);
  box-shadow: 6px 6px 0 var(--ink); }
.gpreview[hidden] { display: none; }
/* The box holds its height whether or not an image is in it. Revealing the
   panel on first selection used to push the tile grid down 383px — the same
   shift-under-the-cursor problem the price rail had, and caused by the same
   instinct to hide a thing until it has content. The empty state fills the
   space AND says what the panel is for. */
.gpreview__box { height: 320px; display: flex; align-items: center; justify-content: center; }
.gpreview__box img { display: block; width: 100%; height: 100%;
  object-fit: contain; object-position: center; }
.gpreview__box img[hidden] { display: none; }
/* #6f675a measured 4.20:1 on --paper-2 at 11.5px — under the 4.5 floor for
   normal-size text. #67625a is 5.00 on paper and is already the grey used
   elsewhere in this file, so this closes the gap without adding a ninth. */
.gpreview__empty { font-family: var(--f-mono); font-size: 11.5px; letter-spacing: 0.06em;
  text-transform: uppercase; color: #67625a; }
.gpreview figcaption { border-top: 2.5px solid var(--ink); background: var(--paper);
  padding: 9px 12px; font-family: var(--f-mono); font-size: 11px; line-height: 1.5; color: #4a443a; }
.gpreview figcaption b { color: var(--ink); font-family: var(--f-body); font-weight: 700; font-size: 12.5px; }
@media (max-width: 620px) { .gpreview img { height: 240px; } }

.presetgrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.preset {
  border: 2.5px solid var(--ink); background: var(--paper); padding: 14px; cursor: pointer; text-align: left;
  font-family: var(--f-body);
  /* Was `transition: 0.12s` — the `all` shorthand, which also animated the
     background and the .preset.on colour swap below. The cyan then FADED in on
     select, which reads as loading rather than committing. Scoped to the two
     properties that actually move. */
  transition: transform var(--dur-state) var(--ease-state), box-shadow var(--dur-state) var(--ease-state);
}
/* Hover is gated on a FINE POINTER. On touch, :hover latches after a tap and
   persists until the next tap elsewhere — and because the hover shadow used to
   be byte-identical to .preset.on's, tapping tile A then tile B left BOTH
   looking selected. Gate on pointer, never on width: a touchscreen laptop at
   1440px is a coarse-pointer device and a width query gets it wrong. */
@media (pointer: fine) {
  .preset:hover { transform: translate(-2px, -2px); box-shadow: 4px 4px 0 var(--ink); }
}
/* Touch needs its own acknowledgement now that hover is gated away: the tile
   presses INTO its own offset shadow, the inverse of the hover lift. */
.preset:active { transform: translate(1px, 1px); box-shadow: 1px 1px 0 var(--ink); }
/* The primary control on the page had no focus style at all. This is the
   convention already used by .dtf-drop, .dtf-ai__preset and .customsize input. */
.preset:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
/* Selected carries THREE signals, not one: the cyan fill, a solid ink bar down
   the leading edge, and no transform. Colour alone failed greyscale, failed
   under the latched-hover bug, and failed anyone who cannot separate cyan from
   paper. The inset bar survives all three. */
.preset.on {
  background: var(--cyan);
  box-shadow: inset 5px 0 0 var(--ink), 4px 4px 0 var(--ink);
}
/* The selected tile swaps to cyan, and the muted greys chosen for the paper
   ground drop to ~3.49 on it. Scope the darker ink to the .on state so the
   unselected tiles keep their hierarchy. */
.preset.on .dim, .preset.on span { color: #151310; }  /* 6.72 on --cyan */
/* 15px at line-height 1.2, not 16px at the inherited 1.5. Display type set at
   body leading is what made the tile read slack — a 16px Syne line was occupying
   24px, roughly 11px of it leading nobody asked for. */
.preset .nm { display: block; font-family: var(--f-display); font-weight: 800; text-transform: uppercase; font-size: 15px; line-height: 1.2; letter-spacing: -0.01em; }
.preset .dim { display: block; font-family: var(--f-mono); font-size: 11px; line-height: 1.4; color: #4a443a; margin: 3px 0 0; }
/* What the size is FOR. One line, 22 characters max — see PRESET_USE in
   custom-dtf.js for why that budget exists and why the copy lives there. */
.preset .use { display: block; font-size: 11px; line-height: 1.35; color: #4a443a; margin: 5px 0 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Price and area share one baseline, divided from the text above. This row is
   the whole reason the area figure is legible: Square and A4 both read $10.95,
   and 625 cm² / 624 cm² sitting opposite makes that read as deliberate. */
.preset .foot { display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  margin-top: 7px; padding-top: 7px; border-top: 1px solid rgba(21,19,16,0.16); }
.preset .pr { font-family: var(--f-display); font-weight: 800; font-size: 18px; line-height: 1.1; }
.preset .area { font-family: var(--f-mono); font-size: 11px; color: #4a443a; white-space: nowrap; }
/* The selected tile's cyan ground needs the darker ink on the footer rule too,
   or it sits at ~3.49:1 like the greys the rule above already fixes. */
.preset.on .foot { border-top-color: rgba(21,19,16,0.34); }
@media (max-width: 620px) {
  .presetgrid { grid-template-columns: repeat(2, 1fr); }
  /* The placement line goes at 2-col. Ten repeated 22-character captions cost
     ~170px of phone scroll to say something the tile above already implies, and
     the cell is too narrow to hold it beside the name without truncating. */
  .preset .use { display: none; }
  .preset .nm { font-size: 14px; }
  /* The price and the area figure sat on ONE baseline row with the area set to
     `white-space: nowrap`, so at 320px every one of the ten tiles pushed its
     area figure 17-25px PAST the card's own 2.5px border and into the gutter —
     measured, and sliced, because body carries `overflow-x: hidden` so there is
     no page-level symptom to notice. Letting the row wrap drops the figure onto
     its own line instead. */
  .preset .foot { flex-wrap: wrap; }
  .preset .area { white-space: normal; }
}
/* ── 2026-07-14 mobile audit sent this to ONE column ≤440px, because the preset
   name "Oversized" at 16px could not fit a 2-col cell on a phone.
   ── 2026-09-16: the single-column rule is GONE. The name now steps to 14px at
   ≤620px, and re-measured in a browser at 320px in a 2-col grid (123px tile,
   91px usable) "Oversized" fits on one line — the stated cause is removed, so
   the workaround goes with it. Ten stacked full-width cards cost ~1288px of
   scroll against ~638px two-up.

   Measured, not assumed, because this reverses a decision made from a real
   audit. What the re-measure DID find: "Full Chest" wraps to two lines at 320px
   — two words, not a length problem the font size can fix. Nothing truncates or
   overflows, and grid stretches its row, so this is accepted as cosmetic rather
   than papered over. If it ever needs fixing, shorten the label, don't restore
   the column rule. ── */
@media (max-width: 440px) {
  .pricepanel { padding: 22px; }
}
.customsize { margin-top: 22px; border-top: 2px solid var(--ink); padding-top: 22px; }
.customsize .frow { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; align-items: end; }
.customsize label { display: block; font-family: var(--f-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 6px; }
.customsize input, .customsize select { width: 100%; font-family: var(--f-body); font-size: 15px; padding: 11px 12px; border: 2px solid var(--ink); background: var(--paper); }
.customsize input:focus, .customsize select:focus { outline: 3px solid var(--ink); }
@media (max-width: 620px) { .customsize .frow { grid-template-columns: 1fr; } }

.pricepanel { border: 2.5px solid var(--ink); background: var(--ink); color: var(--paper); padding: 28px; box-shadow: 10px 10px 0 var(--mag); position: sticky; top: 90px; }
.pricepanel h3, .pricepanel h2 { font-family: var(--f-mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: #b7b1a4; margin-bottom: 6px; }
.pricepanel .total { font-family: var(--f-display); font-weight: 800; font-size: 56px; line-height: 1; color: var(--yel); }
.pricepanel .per { font-family: var(--f-mono); font-size: 12px; color: #c9c3b6; margin-top: 6px; }
/* RESERVED HEIGHT. #dtf-add sits below this block inside a sticky rail, and the
   money rows genuinely come and go — express only when chosen, the volume
   discount only past a tier, add-on lines only when bought. Each is ~31px, so
   changing quantity across a tier boundary physically moved the buy button
   under the customer's cursor mid-click.

   170px = FOUR ROWS, MEASURED ON PRODUCTION, not derived from the font size: a
   row is 37px (not the 31px this was first written for) and .lines carries ~22px
   of its own border and margin, so 3 rows = 133px and 4 rows = 170px. The first
   value tried here was 124px, which is BELOW the three-row height and therefore
   never bound at all — the button still moved 37px. Measure before changing it.

   Four rows, not the worst case: the transition that actually happens under the
   customer's hand is 3 -> 4, when quantity crosses the first volume tier at 10.
   Reserving all six possible rows would leave a dead gap on every simple order,
   which is the objection UI raised. Express and add-ons can still grow the panel
   past this, but those are deliberate choices made once, not something that
   moves while someone is adjusting a number. */
.pricepanel .lines { margin: 20px 0; border-top: 1px solid rgba(239,233,219,0.2); min-height: 170px; }
.pricepanel .lines .l { display: flex; justify-content: space-between; gap: 14px; padding: 8px 0; font-size: 13.5px; border-bottom: 1px solid rgba(239,233,219,0.12); }
/* space-between alone only separates these when the value is SHORT. The
   shipping line's value is a sentence that fills the row, so it ran straight
   into its label and painted "Shipping$10.00". The gap is what actually
   guarantees the separation; the label must not collapse either. */
.pricepanel .lines .l > span:first-child { flex: 0 0 auto; }
.pricepanel .lines .l.save { color: var(--cyan); }
/* D4 — a condition on the promise (the express cut-off), not a charge. Muted
   so it reads as a qualifier beside the money, but #c9c3b6 on the panel's ink
   ground is 10.56:1 — quiet is not the same as low-contrast. */
.pricepanel .lines .l.note { color: #c9c3b6; font-size: 12.5px; }
.pricepanel .lines .l b { font-family: var(--f-mono); }

/* The advisory block. One paragraph, sentences joined by a middot, instead of
   four separate rows that each added or removed ~31px above the buy button.
   #c9c3b6 on --ink is 10.56:1 (already documented above) - quiet, not low
   contrast. min-height holds two lines so the common case does not resize the
   panel as conditions change; longer runs still wrap and grow, once. */
.pricepanel__fine {
  margin: 12px 0 0; font-size: 12px; line-height: 1.45; color: #c9c3b6;
  min-height: 2.9em;
}
.pricepanel__fine .pricepanel__stale {
  display: block; margin-bottom: 6px; padding-left: 9px;
  border-left: 3px solid var(--mag); color: var(--paper);
}

/* ---- trade savings calculator ---- */
.savecalc { border: 2.5px solid var(--ink); background: var(--paper-2); padding: 30px; box-shadow: 8px 8px 0 var(--cyan); }
.savecalc label { font-family: var(--f-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; display: block; margin-bottom: 10px; }
.savecalc input[type=range] { width: 100%; accent-color: var(--mag); }
.savecalc .spend { font-family: var(--f-display); font-weight: 800; font-size: 40px; }
.savecalc .out { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 22px; }
.savecalc .out .box { border: 2.5px solid var(--ink); padding: 18px; text-align: center; }
.savecalc .out .box:nth-child(1) { background: var(--cyan); }
.savecalc .out .box:nth-child(2) { background: var(--mag-ground); color: var(--paper); }
.savecalc .out .box:nth-child(3) { background: var(--yel); }
.savecalc .out .box .k { font-family: var(--f-mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; }
.savecalc .out .box .v { font-family: var(--f-display); font-weight: 800; font-size: 30px; line-height: 1.1; margin-top: 4px; }
/* ── 2026-07-14 mobile audit: 3 stat boxes always in a row → stack ≤560px;
   shrink the DTF price figure so its min-content can't widen the calc panel;
   min-width:0 stops any card/track from blowing past a narrow viewport. ── */
.savecalc .out > *, .tiercards > *, .calc > *, .presetgrid > * { min-width: 0; }
@media (max-width: 560px) { .savecalc .out { grid-template-columns: 1fr; } .pricepanel .total { font-size: clamp(30px, 9vw, 44px); } }

/* ---- tier detail cards ---- */
.tiercards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; align-items: start; }
.tiercard { border: 2.5px solid var(--ink); padding: 24px; box-shadow: 6px 6px 0 var(--ink); }
.tiercard:nth-child(1) { background: var(--paper-2); }
.tiercard:nth-child(2) { background: var(--cyan); }
.tiercard:nth-child(3) { background: var(--mag-ground); color: var(--paper); }
.tiercard:nth-child(4) { background: var(--yel); }
.tiercard .pct { font-family: var(--f-display); font-weight: 800; font-size: 44px; line-height: 1; }
/* Clamped exactly like `.tier h4` in content.css: 20px flat broke "Enterprise"
   onto two lines in a narrow card. Same cap, so wide screens are unchanged. */
.tiercard h4, .tiercard h3, .tiercard h2 { font-family: var(--f-display); font-weight: 800; text-transform: uppercase; font-size: clamp(15px, 1.55vw, 20px); margin: 6px 0 4px; }
.tiercard .th { font-family: var(--f-mono); font-size: 11px; margin-bottom: 14px; }
.tiercard ul { list-style: none; }
.tiercard li { font-size: 13px; padding: 6px 0 6px 18px; position: relative; border-top: 1px solid rgba(21,19,16,0.15); }
.tiercard:nth-child(3) li { border-top-color: rgba(239,233,219,0.25); }
.tiercard li::before { content: "✓"; position: absolute; left: 0; font-weight: 700; }
/* 820px, NOT 900px. `.tiercard` (here) and `.tier` (content.css) are the SAME
   four-card trade ladder with two stylesheets, and their 2-column breakpoints
   had drifted apart — so between 821px and 900px the identical component
   rendered 4-up on /pricing/ and 2-up on /trade-account/. Matched to content.css.
   ⚠ These two must move together; better still, collapse them into one
   component. The remaining differences are the `.pct` size (44 here vs 46) and
   the per-tier perk bullets, which only exist on this page. */
@media (max-width: 820px) { .tiercards { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .tiercards { grid-template-columns: 1fr; } }

/* ---- multi-step wizard ---- */
.wizard { border: 2.5px solid var(--ink); background: var(--paper-2); padding: 30px; box-shadow: 8px 8px 0 var(--mag); max-width: 760px; }
.stepsind { display: flex; gap: 8px; margin-bottom: 24px; }
.stepsind .dot { flex: 1; height: 8px; background: var(--paper); border: 2px solid var(--ink); }
.stepsind .dot.on { background: var(--cyan); }
.wstep { display: none; }
.wstep.on { display: block; }
.wstep h3 { font-family: var(--f-display); font-weight: 800; text-transform: uppercase; font-size: 22px; margin-bottom: 16px; letter-spacing: -0.01em; }
.wnav { display: flex; justify-content: space-between; margin-top: 24px; gap: 14px; }
.wdone { display: none; text-align: center; padding: 20px 0; }
.wdone.on { display: block; }
.wdone .tick { width: 60px; height: 60px; border: 3px solid var(--ink); background: var(--cyan); display: grid; place-items: center; font-size: 30px; margin: 0 auto 18px; }

/* Mobile headline scale (must load AFTER components.css base .op clamp) */
@media (max-width: 560px) {
  .op { font-size: clamp(24px, 9.5vw, 38px); }
  .page-hero .op { font-size: clamp(24px, 9vw, 38px); }
  /* `white-space: normal` moved to the BASE `.op .ln` rule in components.css
     2026-09-18 — it was needed at every width, not just here. */
  /* multiply overprint layers turn wrapped lines into mush on small screens —
     swap to a single offset shadow (still reads as misregistration) */
  .op .ln .c, .op .ln .m, .op .ln .y { display: none; }
  .op .ln .base { text-shadow: 3px 3px 0 var(--mag); }
}

/* Mobile nav width + stray-decoration guard */
body { overflow-x: hidden; }
@media (max-width: 560px) {
  .nav { padding: 12px 14px; gap: 8px; }
  .brand { font-size: 17px; gap: 7px; }
  .brand .sq i { width: 6px; height: 15px; }
  .nav .links { gap: 10px; }
  .nav .cta { padding: 8px 11px; font-size: 12px; }
}

/* ── Custom DTF ordering UI (v28, 2026-07-09 — turnaround / add-ons / upload / notes / cart) ── */
.dtf-block { margin-top: 18px; }
.dtf-block > label { display: block; font-family: var(--f-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 8px; }
.dtf-block .opt { color: #67625a; font-weight: 400; }  /* 5.00 on #efe9db (was 3.11) */
.dtf-block .req { color: var(--mag-ground); font-weight: 700; }  /* 4.53 on #efe9db (was 3.45) */
.dtf-seg { display: flex; gap: 10px; flex-wrap: wrap; }
/* The transition here listed `transform`, which nothing on this control ever
   changes, and omitted `background`, which .on DOES change — so the cyan snapped
   while the shadow faded and the segment transitioned inconsistently with
   itself. Both halves now name what actually moves. */
.dtf-seg__opt { flex: 1; min-width: 150px; text-align: left; padding: 12px 14px; border: 2px solid var(--ink); background: var(--paper); cursor: pointer; font-family: var(--f-body); transition: background var(--dur-state) var(--ease-state), box-shadow var(--dur-state) var(--ease-state); }
.dtf-seg__opt:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
.dtf-seg__opt b { display: block; font-weight: 700; font-size: 14px; }
.dtf-seg__opt span { font-family: var(--f-mono); font-size: 11px; color: #4a443a; }
.dtf-seg__opt.on { background: var(--cyan); box-shadow: 3px 3px 0 var(--ink); }
/* Same cyan-ground problem as .preset.on: #4a443a reads 3.49 on --cyan. */
.dtf-seg__opt.on span { color: #151310; }  /* 6.72 on --cyan */
.dtf-addons { display: flex; flex-direction: column; gap: 10px; }
.dtf-addon { display: flex; gap: 11px; align-items: flex-start; padding: 12px 14px; border: 2px solid var(--ink); background: var(--paper); cursor: pointer; }
.dtf-addon input { margin-top: 3px; width: 16px; height: 16px; flex: 0 0 auto; accent-color: var(--cyan); }
.dtf-addon__t { font-weight: 600; font-size: 14px; }
.dtf-addon__t b { font-family: var(--f-mono); }
.dtf-addon__g { font-family: var(--f-mono); font-size: 11px; color: #8a8375; }
.dtf-addon__n { display: block; font-size: 12px; color: #4a443a; margin-top: 3px; line-height: 1.4; }
.dtf-addon.gated { opacity: 0.45; cursor: not-allowed; }
.dtf-drop { border: 2px dashed var(--ink); background: var(--paper); padding: 22px 18px; text-align: center; cursor: pointer; transition: background var(--dur-state) var(--ease-state), border-color var(--dur-state) var(--ease-state); }
.dtf-drop:focus-visible { outline: 3px solid var(--ink); outline-offset: 2px; }
.dtf-drop.drag { background: #e6dfcd; }
.dtf-drop.has { border-style: solid; box-shadow: 3px 3px 0 var(--ink); }
/* Was a <div> inside a role="button" wrapper — a button containing the Remove
   button is a nested-interactive failure. The wrapper is now a plain drag
   target and THIS is the real control, so it needs the UA styles removed. */
.dtf-drop__idle { display: block; width: 100%; margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit; text-align: center; cursor: pointer; }
.dtf-drop__idle:focus-visible { outline: 3px solid var(--ink); outline-offset: 4px; }
.dtf-drop__t { display: block; font-family: var(--f-display); font-weight: 800; text-transform: uppercase; font-size: 15px; letter-spacing: -0.01em; }
.dtf-drop__s { display: block; font-size: 12px; color: #4a443a; margin-top: 5px; line-height: 1.45; }
.dtf-drop__has { display: flex; align-items: center; justify-content: center; gap: 14px; flex-wrap: wrap; }
/* paintArtwork() swaps these two panes with the `hidden` PROPERTY, but a class
   selector setting `display` outranks the UA's [hidden] rule, so `hidden` had no
   visual effect: the Remove button and filename row rendered even with no file
   chosen. The class+attribute selector below outranks the plain class and
   restores the intent for BOTH panes — __idle needs it too now that it carries
   its own display:block for the <button> restyle. */
.dtf-drop__idle[hidden], .dtf-drop__has[hidden] { display: none; }
.dtf-drop__name { font-family: var(--f-mono); font-size: 13px; word-break: break-all; }
/* Artwork preview. The CHECKERBOARD is the point: it is the only way to see
   which pixels are transparent, and "transparent" vs "still white" is exactly
   the judgement the Remove-background buttons ask the customer to make. Squares
   are 8px so they stay legible behind fine line work without moiring against it. */
.dtf-drop__thumb { width: 132px; height: 132px; cursor: zoom-in; flex: 0 0 auto; border: 2px solid var(--ink);
  display: inline-block; position: relative; background-color: #fff;
  background-image:
    linear-gradient(45deg, #cfcabb 25%, transparent 25%, transparent 75%, #cfcabb 75%),
    linear-gradient(45deg, #cfcabb 25%, transparent 25%, transparent 75%, #cfcabb 75%);
  background-size: 16px 16px;
  background-position: 0 0, 8px 8px; }
.dtf-drop__thumb[hidden] { display: none; }
.dtf-drop__thumb img { display: block; width: 100%; height: 100%; object-fit: contain; }
.dtf-drop__thumb:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
/* A magnifier badge, so the tile reads as openable rather than decorative. */
.dtf-drop__thumb::after { content: "5"; position: absolute; right: -2px; bottom: -2px;
  width: 22px; height: 22px; line-height: 22px; text-align: center;
  background: var(--ink); color: var(--paper); font-size: 13px; }

/* ── Artwork inspector ─────────────────────────────────────────────────────
   Full-screen because the question it answers — "is this edge clean?" — cannot
   be answered in a panel. Fixed, above everything, and it takes the keyboard
   while open. The checkerboard rides on the IMAGE so the squares scale with the
   artwork: at 8x you are looking at 8x squares, which is what makes a one-pixel
   fringe visible instead of lost in fine checks. */
.ifd-inspect { position: fixed; inset: 0; z-index: 9999; display: flex; flex-direction: column;
  background: rgba(21,19,16,0.93); }
.ifd-inspect[hidden] { display: none; }
.ifd-inspect__bar { display: flex; align-items: center; gap: 8px; padding: 10px 14px;
  background: var(--ink); color: var(--paper); border-bottom: 2px solid var(--cyan); }
.ifd-inspect__name { font-family: var(--f-mono); font-size: 12px; letter-spacing: 0.04em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 46vw; }
.ifd-inspect__spacer { flex: 1 1 auto; }
.ifd-inspect__pct { font-family: var(--f-mono); font-size: 12px; min-width: 62px; text-align: center;
  font-variant-numeric: tabular-nums; }
.ifd-inspect__bar button { border: 2px solid var(--paper); background: transparent; color: var(--paper);
  font-family: var(--f-mono); font-size: 12px; font-weight: 700; padding: 5px 11px; cursor: pointer;
  text-transform: uppercase; }
.ifd-inspect__bar button:hover { background: var(--paper); color: var(--ink); }
.ifd-inspect__bar button:focus-visible { outline: 3px solid var(--cyan); outline-offset: 2px; }
.ifd-inspect__stage { flex: 1 1 auto; display: flex; align-items: center; justify-content: center;
  overflow: hidden; cursor: grab; touch-action: none; }
.ifd-inspect__stage.is-drag { cursor: grabbing; }
.ifd-inspect__stage img { transform-origin: center center; image-rendering: pixelated;
  background-color: #fff; background-repeat: repeat; will-change: transform; }
.ifd-inspect__hint { margin: 0; padding: 9px 14px; text-align: center; color: #cfcabb;
  font-family: var(--f-mono); font-size: 11px; letter-spacing: 0.05em; background: var(--ink); }
@media (max-width: 620px) {
  .ifd-inspect__name { max-width: 30vw; }
  .ifd-inspect__bar button { padding: 5px 8px; }
}
.dtf-drop__x { border: 2px solid var(--ink); background: var(--paper); padding: 5px 12px; font-family: var(--f-mono); font-size: 11px; text-transform: uppercase; cursor: pointer; }
/* Background removal. Same shape as Remove, filled cyan because it is the
   ACTION offered here while Remove is the escape — an outline next to an
   outline gives a customer two equal-looking choices and no steer. */
.dtf-drop__bg { border: 2px solid var(--ink); background: var(--cyan); color: var(--ink);
  padding: 5px 12px; font-family: var(--f-mono); font-size: 11px; font-weight: 700;
  text-transform: uppercase; cursor: pointer;
  transition: box-shadow var(--dur-state) var(--ease-state); }
.dtf-drop__bg[hidden] { display: none; }
@media (pointer: fine) { .dtf-drop__bg:hover { box-shadow: 3px 3px 0 var(--ink); } }
.dtf-drop__bg:active { box-shadow: 1px 1px 0 var(--ink); }
.dtf-drop__bg:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
/* Rule 3 of the semantic scale: disabled is an explicit pair, never opacity —
   opacity over a 2.5px ink border on a halftone gives an unpredictable ratio. */
.dtf-drop__bg:disabled { background: var(--disabled-bg); color: var(--disabled-fg);
  cursor: default; box-shadow: none; }
.dtf-block textarea { width: 100%; font-family: var(--f-body); font-size: 14px; padding: 11px 12px; border: 2px solid var(--ink); background: var(--paper); resize: vertical; }
.dtf-block textarea:focus { outline: 3px solid var(--ink); }
.dtf-msg { margin-top: 14px; padding: 11px 14px; font-size: 13px; border: 2px solid; line-height: 1.5; }
.dtf-msg.ok { background: #ddecd9; border-color: #188743; color: #14532d; }
.dtf-msg.err { background: #f6ddd3; border-color: #c22f2f; color: #7c1d1d; }
.dtf-msg a { color: inherit; font-weight: 700; }

/* ── The four price states ───────────────────────────────────────────────────
   EMPTY / SETTLED carry no class. The two below exist so a figure that is not
   the server's current answer cannot be mistaken for one.

   PENDING runs in two stages on purpose. The dim alone is not enough: a 35%
   opacity "$118.40" is still a perfectly legible "$118.40", so it communicates
   staleness without actually preventing the number being read. Past 400ms the
   glyphs are replaced by a bar of the SAME MEASURED WIDTH (--ghost-w, set in JS
   from getBoundingClientRect before clearing) so nothing reflows.

   STALE is persistent, not a dip — the fetch failed and we are deliberately
   still showing the last good quote. The magenta rule underneath borrows the
   panel's existing box-shadow language rather than inventing a signal. ── */
.pricepanel .total { transition: opacity var(--dur-state) var(--ease-state); }
.pricepanel .total.is-pending { opacity: 0.35; }
.pricepanel .total.is-ghosted {
  color: transparent; position: relative; min-width: var(--ghost-w, 5ch); opacity: 1;
}
.pricepanel .total.is-ghosted::after {
  content: ""; position: absolute; left: 0; top: 18%; height: 64%;
  width: var(--ghost-w, 5ch); background: rgba(239, 233, 219, 0.22);
  background-image: linear-gradient(100deg, transparent 20%, rgba(239,233,219,0.45) 50%, transparent 80%);
  background-size: 220% 100%; animation: ifd-shimmer 1400ms linear infinite;
}
@keyframes ifd-shimmer { from { background-position: 120% 0; } to { background-position: -120% 0; } }
.pricepanel .total.is-stale {
  opacity: 0.55;
  border-bottom: 2px solid var(--mag); padding-bottom: 4px;
}
/* Reduced motion: the STATE must survive, only the loop goes. A static bar is
   still visibly not-a-price; a shimmer is the decoration, not the information. */
@media (prefers-reduced-motion: reduce) {
  .pricepanel .total.is-ghosted::after { animation: none; background-image: none; }
}

/* ── Field-level refusals ────────────────────────────────────────────────────
   tokens.css rule 1: "`refused` and `error` share the danger colour and NOT the
   treatment. A refusal is a 2px --danger border on --danger-tint, INLINE and
   attached to the control... An error is the same colours in a DETACHED toast."

   So the split on this page is by KIND, not by convenience:
     refusal  — a wrong file, an out-of-range size. The customer can fix it right
                there, so it renders against the control. This is .fieldmsg.
     error    — the add-to-cart call failed. Nothing local to fix, the offer is
                "retry", so it stays the detached .dtf-msg toast in the rail.

   Why this matters more than tidiness: the toast lives inside .pricepanel, and
   @media (max-width:900px) drops that panel BELOW the entire form. Every refusal
   was therefore being delivered several screens away from the control that
   caused it — on a phone, dropping a 60MB file looked like nothing happened.

   These use the semantic tokens rather than the hardcoded #c22f2f/#f6ddd3 pair
   above, which tokens.css names as part of the drift the scale was built to end.
   The existing .dtf-msg literals are left alone here; swapping them is its own
   change with its own verification. ── */
.fieldmsg {
  margin: 10px 0 0; padding: 9px 12px; font-size: 13px; line-height: 1.45;
  border: 2px solid var(--danger); background: var(--danger-tint); color: var(--ink);
}
.fieldmsg.warn { border-color: var(--warning); background: var(--warning-tint); }
/* The drop zone itself must visibly refuse. Previously setFile() returned early
   without touching it at all, so the zone looked exactly as it had a moment
   before and gave no sign it had rejected anything. */
.dtf-drop.bad { border-style: solid; border-color: var(--danger); background: var(--danger-tint); }

/* ── AI artwork panel (Phase 7) ──────────────────────────────────────────────
   Sits under the drop zone as the alternative to uploading, deliberately quieter
   than it: most customers arrive with their own file, so this must read as a way
   out for the ones who don't, not as the main path. Collapsed until asked for. */
.dtf-ai { margin-top: 12px; border: 2px solid var(--ink); background: var(--paper-2); }
.dtf-ai__toggle { display: flex; align-items: center; gap: 9px; width: 100%; padding: 11px 14px; border: 0; background: none; font: inherit; font-size: 13px; text-align: left; cursor: pointer; }
.dtf-ai__toggle b { font-family: var(--f-display); font-weight: 800; text-transform: uppercase; letter-spacing: -0.01em; }
.dtf-ai__toggle:focus-visible { outline: 3px solid var(--ink); outline-offset: -3px; }
.dtf-ai__spark { color: var(--mag); font-size: 15px; line-height: 1; }
/* Collapsed by default; the panel is revealed by JS adding .open, so a page with
   no JS never shows a generator that cannot work. */
.dtf-ai__body { display: none; padding: 0 14px 14px; }
.dtf-ai.open .dtf-ai__body { display: block; }
.dtf-ai__lab { display: block; font-family: var(--f-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 6px; }
.dtf-ai__presets { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
.dtf-ai__preset { border: 2px solid var(--ink); background: var(--paper); padding: 5px 10px; font-family: var(--f-mono); font-size: 11px; text-transform: uppercase; cursor: pointer; }
.dtf-ai__preset.on { background: var(--ink); color: var(--paper); }
.dtf-ai__preset:focus-visible { outline: 3px solid var(--ink); outline-offset: 2px; }
.dtf-ai__row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.dtf-ai__go[disabled] { opacity: 0.45; cursor: not-allowed; }
.dtf-ai__quota { font-family: var(--f-mono); font-size: 11px; color: #4a443a; }
.dtf-ai__msg { margin-top: 10px; padding: 9px 12px; font-size: 12.5px; border: 2px solid; line-height: 1.45; }
/* Six literal hex values, re-introduced by the Phase-7 AI panel AFTER the
   semantic scale shipped — tokens.css names these exact strays as the drift the
   scale exists to end. The tokens carry the audited contrast; the literals did
   not. Same pair as .acct-notice--error/--success in account.css. */
.dtf-ai__msg.err { background: var(--danger-tint); border-color: var(--danger); color: var(--ink); }
.dtf-ai__msg.ok { background: var(--success-tint); border-color: var(--success); color: var(--ink); }
.dtf-ai__out { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 10px; margin-top: 12px; }
.dtf-ai__out:empty { margin-top: 0; }
.dtf-ai__r { margin: 0; border: 2px solid var(--ink); background: var(--paper); display: flex; flex-direction: column; }
.dtf-ai__r.on { box-shadow: 3px 3px 0 var(--mag); }
/* A generation is cut out, so it has NO background of its own — the chequerboard
   is what makes transparency legible instead of looking like a white square. */
.dtf-ai__r img { width: 100%; aspect-ratio: 1; object-fit: contain; display: block;
  background-color: #fff;
  background-image: linear-gradient(45deg, #e2ddd0 25%, transparent 25%), linear-gradient(-45deg, #e2ddd0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e2ddd0 75%), linear-gradient(-45deg, transparent 75%, #e2ddd0 75%);
  background-size: 14px 14px; background-position: 0 0, 0 7px, 7px -7px, -7px 0; }
.dtf-ai__r figcaption { font-size: 11px; line-height: 1.35; padding: 6px 7px 0; color: #4a443a;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.dtf-ai__use { margin: 6px; margin-top: auto; border: 2px solid var(--ink); background: var(--paper); padding: 5px 8px; font-family: var(--f-mono); font-size: 10.5px; text-transform: uppercase; cursor: pointer; }
.dtf-ai__r.on .dtf-ai__use { background: var(--ink); color: var(--paper); }
.dtf-ai__fine { font-size: 11px; color: #6a6357; line-height: 1.45; margin: 12px 0 0; }
/* An AI design is confirmed by name in the drop zone, not by a second thumbnail
   — the panel below already shows the picture. */
.dtf-drop.is-ai { border-color: var(--mag); }
.dtf-addon.moot { opacity: 0.45; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) { .dtf-ai, .dtf-ai * { transition: none !important; } }

/* `font: inherit` was the SHORTHAND, so it reset size AND weight — and `button.btn`
   (0,1,1) outranks `.btn` (0,1,0). Measured on production 2026-09-18: every
   <button class="btn"> rendered 16px/400 while sibling <a class="btn"> rendered
   15px/700 — including "Add to cart →" and "Generate" on the money path.
   base.css already sets `button { font-family: inherit }`, which is the only part
   that was ever wanted, so the declaration was redundant as well as harmful. */
button.btn { cursor: pointer; }

/* ── Contact Form 7 (v28) — form id 19, rendered by page-static do_shortcode ── */
.cf7-form .wpcf7 { margin: 0; }
.cf7-field-group { margin-bottom: 18px; }
/* DESCENDANT, not child: CF7 wraps every label in its own <p>, so `>` matched
   nothing and all five labels fell back to 16px body copy. Verified on prod:
   label-as-direct-child 0, label-inside-<p> 5. */
.cf7-field-group label { display: block; font-family: var(--f-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 8px; }
/* --mag is 3.45:1 on paper and illegal for 11px text. --mag-ground is 4.53.
   Same fix, same reason, as .dtf-block .req above. */
.cf7-field-group abbr { color: var(--mag-ground); text-decoration: none; }
.cf7-form input[type="text"], .cf7-form input[type="email"], .cf7-form input[type="tel"],
.cf7-form select, .cf7-form textarea {
  width: 100%; font-family: var(--f-body); font-size: 15px; padding: 12px 13px;
  border: 2px solid var(--ink); background: var(--paper); color: var(--ink); border-radius: 0;
}
.cf7-form textarea { min-height: 130px; resize: vertical; }
.cf7-form input:focus, .cf7-form select:focus, .cf7-form textarea:focus { outline: 3px solid var(--ink); outline-offset: 0; }
.cf7-form .wpcf7-submit {
  width: 100%; margin-top: 6px; padding: 14px 22px; cursor: pointer;
  font: 700 15px/1 var(--f-display); text-transform: uppercase; letter-spacing: 0.04em;
  background: var(--cyan); color: var(--ink); border: 2px solid var(--ink); box-shadow: 4px 4px 0 var(--ink);
  transition: transform 0.15s, box-shadow 0.15s;
}
.cf7-form .wpcf7-submit:hover { transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--ink); }
.cf7-form .wpcf7-submit:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink); }
.cf7-form .wpcf7-spinner { margin: 12px auto 0; display: block; }
/* validation + response */
/* These six literals were the exact strays tokens.css names as the drift the
   semantic scale exists to end. --danger is 6.04:1 and measured; #c22f2f was
   4.64 and was not. This is the page a customer actually sees them on. */
/* The form's fallback route. Quiet by design - it must be findable when the
   form is gone, without competing with the form when it is there. Mono at the
   same 11px/0.1em as the field labels, so it reads as part of the form block. */
.cf7-fallback { margin-top: 18px; padding-top: 16px; border-top: 2px solid var(--line);
  font-family: var(--f-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; line-height: 1.7;
  /* #4a443a is the literal, not a token reference, deliberately. There is no
     --muted token: the audit counted SEVEN un-named muted greys (#3a352d,
     #2e2a23, #4a443a and five more) across four stylesheets and named that as
     the real gap in the scale. Writing var(--muted, #4a443a) here would read
     as token discipline while always falling back - worse than the literal,
     because it hides the gap instead of leaving it visible. 7.96:1 on paper. */
  color: #4a443a; }
/* The links opt OUT of the uppercase. Caught by looking at the render: the
   label treatment was shouting the email address as HELLO@INKFUSIONDTF.COM.AU,
   which is a string someone may have to read back or type. The lead-in stays
   mono caps so the block still reads as part of the form. */
.cf7-fallback a { color: var(--ink); text-transform: none; font-size: 12px;
  letter-spacing: 0.02em; text-decoration: underline; text-underline-offset: 3px; }
.cf7-fallback a:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }

.cf7-form .wpcf7-not-valid { border-color: var(--danger); }
.cf7-form .wpcf7-not-valid-tip { color: var(--danger); font-family: var(--f-mono); font-size: 11px; margin-top: 5px; }
.cf7-form .wpcf7-response-output {
  margin: 18px 0 0 !important; padding: 12px 15px !important; border: 2px solid var(--ink) !important;
  font-size: 13.5px; font-family: var(--f-body);
}
.cf7-form form.sent .wpcf7-response-output { background: var(--success-tint); border-color: var(--success) !important; color: var(--ink); }
.cf7-form form.invalid .wpcf7-response-output, .cf7-form form.failed .wpcf7-response-output { background: var(--danger-tint); border-color: var(--danger) !important; color: var(--ink); }

/* Contact map embed (v28) */
.dtf-map { border: 2.5px solid var(--ink); box-shadow: 8px 8px 0 rgba(21,19,16,0.14); line-height: 0; background: var(--paper-2); }
.dtf-map iframe { display: block; width: 100% !important; height: 420px !important; border: 0 !important; border-radius: 0 !important; }
@media (max-width: 620px) { .dtf-map iframe { height: 320px !important; } }

/* ─── R2 · "which should I order?" routing block ──────────────────────────
   Rendered by [ifd_routing_block]. v28 idiom: square, 2.5px ink borders, hard
   shadow, paper ground. Contrast on paper: ink 15.32:1, muted 6.26:1, labels
   4.78:1 — all AA, and the borders are non-text at 15.32:1 against 3:1.
   No colour is defined only inside a media query. */
.ifd-routing { margin: 48px 0; }
.ifd-routing__h { font-family: var(--f-display); font-weight: 800; font-size: clamp(24px, 4vw, 38px); color: var(--ink); margin: 0 0 8px; text-wrap: balance; }
.ifd-routing__lead { max-width: 62ch; color: #5b5346; margin: 0 0 26px; line-height: 1.6; }
.ifd-routing__grid { list-style: none; margin: 0; padding: 0; display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.ifd-routing__card { display: flex; flex-direction: column; border: 2.5px solid var(--ink); background: var(--paper); box-shadow: 8px 8px 0 var(--cyan); padding: 22px; }
.ifd-routing__card:nth-child(2) { box-shadow: 8px 8px 0 var(--mag); }
.ifd-routing__card:nth-child(3) { box-shadow: 8px 8px 0 var(--yel); }
.ifd-routing__name { font-family: var(--f-display); font-weight: 800; font-size: 19px; color: var(--ink); margin: 0 0 2px; }
.ifd-routing__sub { font-family: var(--f-mono); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: #6b6558; margin: 0 0 16px; }
.ifd-routing__facts { margin: 0 0 20px; display: grid; grid-template-columns: auto; gap: 2px 0; }
.ifd-routing__facts dt { font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .09em; text-transform: uppercase; color: #6b6558; margin-top: 12px; }
.ifd-routing__facts dd { margin: 0; color: var(--ink); font-size: 14.5px; line-height: 1.5; }
/* The CTA sits flush at the card's foot whatever the fact list's height, so a
   row of cards reads as one row rather than three ragged ones. */
.ifd-routing__go { margin-top: auto; display: inline-block; border: 2.5px solid var(--ink); background: var(--ink); color: var(--paper); text-decoration: none; font-family: var(--f-mono); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; padding: 11px 14px; text-align: center; }
.ifd-routing__go:hover, .ifd-routing__go:focus-visible { background: var(--paper); color: var(--ink); }
.ifd-routing__go:focus-visible { outline: 3px solid var(--cyan); outline-offset: 2px; }

/* ─── Component · [ifd_no_charges] ────────────────────────────────────────
   A trust block, so it stays quiet: no shadows, no accent fills competing with
   the price tables it sits near. The rule above each item does the separating.
   #5b5346 on paper is 6.26:1 (AA). */
.ifd-nocharge { margin: 40px 0; }
.ifd-nocharge__grid { list-style: none; margin: 0; padding: 0; display: grid; gap: 24px 32px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.ifd-nocharge__item { border-top: 2.5px solid var(--ink); padding-top: 14px; }
.ifd-nocharge__t { font-family: var(--f-display); font-weight: 800; font-size: 16px; color: var(--ink); margin: 0 0 6px; text-wrap: balance; }
.ifd-nocharge__b { margin: 0; font-size: 14px; line-height: 1.6; color: #5b5346; }


/* ─── HERO FLIP WORD MUST NOT SCROLL THE PAGE — 2026-09-11 ────────────────
   The home H1 ends in a scramble animation cycling MERCH / GANG SHEETS /
   TRANSFERS. Measured at a REAL 375px viewport (device emulation, not a
   narrowed window — the window would not go below 485px and hid this):
   "TRANSFERS" overflowed by 6px at rest and was visibly clipped mid-word,
   while the scrambled intermediate frames, which use wider glyphs like # and
   W, reached +65px. The document scrolled sideways 7px on every phone.

   9.5vw = 35.6px at 375px and TRANSFERS needs ~33px to fit the 335px heading,
   so 8.6vw (32.3px) leaves a little headroom. Clipping the wrapper stops any
   transient scramble frame from widening the layout on its own. ── */
@media (max-width: 420px) {
  .op { font-size: clamp(22px, 8.6vw, 38px); }
  .flipwrap { max-width: 100%; overflow-x: clip; }
}

/* ─── ROTATED BAND MUST NOT SCROLL THE PAGE — 2026-09-11 ──────────────────
   .carwrap is a full-width band with transform: rotate(-2.5deg). A rotated
   box's BOUNDING box is wider than its layout box, so at 375px it measured
   388px wide and pushed the document to scrollWidth 382 — 7px of sideways
   scroll on every phone. It was the last remaining overflow after the hero
   flip word was fixed.

   `overflow-x: hidden` on body does not stop this: with html at overflow
   visible the value propagates to the viewport instead of clipping here.
   Clipped on the band's own section instead — verified it contains no
   position:sticky descendants, and `clip` (unlike `hidden`) creates no scroll
   container, so nothing sticky elsewhere can break. ── */
.blk:has(> .carwrap) { overflow-x: clip; }


/* ── Reduced motion, for the DTF calculator only ───────────────────────────
   base.css carries a blanket `transition-duration: 0.01s !important` on `*`
   under prefers-reduced-motion. That is a kill switch, not a reduction: it
   makes every state change TELEPORT, and a change that teleports communicates
   less than one that fades quickly. It also silently outranks any tokenised
   value, so the shortened durations in tokens.css would never apply.

   Rather than weaken the global rule — which still usefully backstops
   third-party and legacy CSS across the site — this re-asserts the tokens for
   the one component built against them. `#dtf-calc *` outranks `*`, and both
   carry !important, so the more specific selector wins.

   TRANSFORMS are dropped entirely here: translate and scale are the parts with
   a vestibular cost. Opacity and colour keep their (shortened) transitions,
   which is the "replace, don't remove" half of the guidance. ── */
@media (prefers-reduced-motion: reduce) {
  #dtf-calc, #dtf-calc *, #dtf-calc *::before, #dtf-calc *::after {
    transition-duration: var(--dur-state) !important;
  }
  #dtf-calc .preset:hover, #dtf-calc .preset:active { transform: none !important; }
  #dtf-calc .gopt:hover, #dtf-calc .gopt:active { transform: none !important; }
}
