/* The wizard chrome only. The preview iframe styles itself from the generated page, so
   nothing in here may leak into it - which is the whole reason it is an iframe. */

.forge-wizard {
  display: grid;
  grid-template-columns: minmax(380px, 4fr) 6fr;
  gap: 0;
  min-height: 100vh;
  min-height: 100dvh;

  /* THE BOUNDARY TOKEN. --line is rgba(hue,.16) and composites to 1.32 / 1.27 / 1.37 against
     --body-bg on amber / green / USA. A text field with `background: transparent` and a 1.3:1
     border has no perceivable boundary at all - on the first card of the wizard, "What is your
     business called?" is a caret floating in empty space. WCAG 1.4.11 asks 3:1.
     --line-strong (.34) reaches only 2.15 / 1.87 / 2.06 and --line-hover (.5) FAILS green at
     2.69, so neither shipped token is enough. --muted-2 is the one that clears every theme on
     both sides: 6.16 / 6.08 / 4.93 against the page and 3.19 / 3.12 / 4.93 against the white
     preview frame. Computed with forge/tokens.js contrastRatio() + composite.js over(). */
  --fw-edge: var(--muted-2, #767676);

  /* /build is the only page on the site with transparent-background text inputs, and the only
     one that declares no color-scheme - so the UA assumes `light` and paints a BLACK caret and
     a near-white autofill wash into a transparent field sitting on a near-black page. Declared
     here rather than as a <meta> because the meta is document-wide and would be a lie on the
     USA theme, whose --body-bg is #ffffff. color-scheme applies per element subtree. */
  color-scheme: dark;
}
[data-theme="red"] .forge-wizard { color-scheme: light; }

.fw-pane { min-width: 0; }

.fw-pane-question {
  display: flex;
  flex-direction: column;
  gap: 24px;
  padding: 28px clamp(20px, 3vw, 40px) 28px;
  border-right: 1px solid var(--line, #2a2a2a);
}

.fw-head { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.fw-home { font-weight: 700; text-decoration: none; margin-right: auto; line-height: 44px; }
.fw-progress { margin: 0; font-size: .9rem; opacity: .75; }

/* A RING, NOT A FILL, AND THE ARITHMETIC SAYS IT HAS TO BE. The unfilled remainder is half
   the information a progress bar carries and it sat at 1.34 / 1.37 / 1.33 against the page -
   invisible. Filling the track with --fw-edge is WRONG: accent-vs--muted-2 is 1.74 / 1.19 /
   1.36, so the fill would vanish into the track. And no solid colour can satisfy both sides
   on green: contrast(track,bg)>=3 needs L>=0.1163 while contrast(accent,track)>=3 needs
   L<=0.0841 - an empty interval. A 1px ring marks the extent (6.16 / 6.08 / 4.93) while the
   fill/track edge keeps carrying the value (8.02 / 5.30 / 5.03). */
.fw-bar { flex: 1 0 100%; height: 4px; border-radius: 4px; background: rgba(128,128,128,.25);
          box-shadow: 0 0 0 1px var(--fw-edge, #767676); overflow: hidden; }
.fw-bar-fill { display: block; height: 100%; width: 0; background: var(--accent, currentColor); transition: width .25s ease; }

/* TWO BARS, AND THE FIRST ONE NEVER RETREATS.
   The easy path is 18 cards and the advanced path is 24. One bar measured against one
   total falls from 94% to 71% at the exact moment a prospect opts into MORE work, which
   punishes the only person in the funnel who engaged. So the run's own bar fills and stays
   full, and the adjustments get their own track underneath, which exists only once they do.
   Stacked rather than side by side on purpose: sharing a row would shrink the first bar's
   pixel width the instant the second appeared, and "nothing moves" is the whole point. */
/* TWO OPACITIES MULTIPLIED. .85 on the group times .55 on the fill is an effective .4675,
   which puts the second bar's fill at 2.90 / 2.27 / 2.21 against its own track - under the
   3:1 that 1.4.11 asks of a progress indicator, so the adjustments bar reads as an empty
   track on all three themes. Collapsing to a single .7 gives 4.68 / 3.40 / 3.32 and keeps
   the intent (quieter than the main bar's 1.0) that the two-bar comment above is about.
   Dropping the group opacity also matters now that .fw-bar carries the --fw-edge ring from
   C1: a .85 group would have dimmed the ring this bar shares with the first one. */
.fw-bar-extra { margin-top: -6px; }
.fw-bar-extra .fw-bar-fill { background: var(--accent, currentColor); opacity: .7; }

/* THE SCOPE BOUNDARY, MOVED TO THE FOOT OF THE PANE (round 2, item B). It is still on
   screen for the reason the design's risk table gives - a prospect who expected a booking
   system and got a brochure site is a worse outcome than one who never started - but it is
   true once, and it was being read out above all 24 questions.
   NOTHING POSITIONS IT: build/index.html puts it after .fw-actions and .fw-card is
   `flex: 1 1 auto` in this pane's column, so the card absorbs the slack and the actions plus
   this line sit on the pane's bottom edge at every height. `margin-top: auto` was REJECTED
   for that reason - it would be a second mechanism doing the same job, and on a short
   viewport (where the card has no slack to give) it would do nothing at all.
   --muted-2 rather than --muted: this is now a footnote under the controls rather than a
   preamble above the question, and it should read quieter than the answer. 6.16 / 6.08 / 4.93
   on amber / green / USA, so it clears AA on all three (the arithmetic is at .fw-facet-label).
   NO opacity, and no separator rule. The opacity is forbidden outright - this element
   contains an --accent link, and a .7 group composited it to 5.59 / 4.11 / 3.89, which fails
   AA on two themes (see .fw-link). A `border-top` hairline was REJECTED on the same file's
   own C1 gate: --line is 1.32:1 against the page, and the only token that clears 3:1 is
   --fw-edge at 6.16, which is a loud rule for a quiet line. */
.fw-scope { margin: 0; font-size: .85rem; color: var(--muted-2, #767676); max-width: 60ch; }

/* THE WIZARD'S ONLY TWO WAYS OUT TO THE CONTACT FORM, AND BOTH WERE INVISIBLE.
   styles.css:233 is `a { color: inherit; text-decoration: none; }` site-wide, and nothing
   in wizard.css overrode it - so these rendered as ordinary sentence text: same colour, no
   underline, no affordance. Separately they measured 117x18 and 69x21 at 390. An in-sentence
   link is exempt under WCAG 2.5.8's inline clause, but every generated page clears 44px and
   that is the bar here. block + fit-content gives a deterministic 44px box at every width;
   inline-block was REJECTED because its height then depends on where the sentence happens to
   wrap, and padding + negative margin was REJECTED because it hides a 26px overhang that
   overlaps the neighbouring text lines and nobody can see. line-height:44px is the same
   device forge/base-css.js:175 already uses for .forge-nav-brand.
   .fw-scope's opacity:.7 had to go too: an --accent link seen through a .7 group composites
   to 5.59 / 4.11 / 3.89 on amber / green / USA, i.e. it FAILS AA on two of three themes.
   At full strength it is 10.73 / 7.26 / 6.70. */
.fw-link {
  display: block;
  width: fit-content;
  line-height: 44px;
  color: var(--accent, currentColor);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.fw-card { flex: 1 1 auto; }
.fw-prompt { font-size: clamp(1.5rem, 3vw, 2.1rem); line-height: 1.2; margin: 0 0 .4em; }
.fw-hint { margin: 0 0 1.2em; opacity: .78; max-width: 54ch; }
.fw-payoff { margin: 1.4em 0 0; font-size: .92rem; opacity: .7; max-width: 54ch; }

.fw-input { display: flex; flex-direction: column; gap: 10px; max-width: 46ch; }
/* A twenty-tile picker is the widest thing in the wizard and must not inherit the cap that
   keeps a business-name field from being a metre wide. Set from JS on picker cards only,
   rather than with :has(), so the rule cannot depend on selector support.
   SPECIFICITY, NEVER SOURCE ORDER: (0,2,0) over .fw-input's (0,1,0), the same lift
   .fw-pane-question .fw-input-rows already carries and for the same reason. Until
   2026-09-12 this was a bare `.fw-input-wide { max-width: none }` at (0,1,0) against the
   46ch cap on the SAME element - a tie, decided by which line came last, in a file whose own
   law forbids exactly that. It read correctly for the whole time it shipped, which is what
   makes it worth fixing rather than leaving: it was one reordered rule away from capping the
   twenty-tile picker at 46ch, with nothing failing anywhere.
   The ancestor is the pane rather than .fw-card, so the selector says where the cap applies
   (the question column) rather than which panel happens to hold it. */
.fw-pane-question .fw-input-wide { max-width: none; }
/* ONE RULE, TWO SCOPES, AND THE SECOND ONE IS PART B'S. The quote form and the "Have a code?"
   box are not inside #forge-input - renderCard() calls replaceChildren() on that element for
   every card, so nothing that has to survive a card change may live in it - and a field with
   no rule here is a transparent box with the UA's 1px inset border on a near-black page,
   which is the 1.3:1 boundary the --fw-edge note at the top of this file exists to fix.
   `.fw-field input` rather than `.fw-quote input`: the wrapper label is what the wizard puts
   round every captioned control already, so this covers both new panels and anything captioned
   later, and it deliberately does NOT reach the quote form's honeypot, which is the one input
   on the page that must stay 1px tall and invisible (styles.css `.hp`, at (0,1,0) - a
   `.fw-quote input` rule would be (0,1,1) and would beat it, putting the trap on screen).
   Same specificity as the .fw-input pair and the identical declarations, so where both match
   a field inside a card there is no outcome for source order to decide. */
.fw-input input, .fw-input textarea,
.fw-field input, .fw-field textarea {
  width: 100%; padding: 12px 14px; font-size: 16px; font-family: inherit;
  border: 1px solid var(--fw-edge, #767676); border-radius: 8px;
  background: transparent; color: inherit;
}
/* THE THIRTY-SIX CONTROLS B11 DID NOT REACH.
   B11 rang .fw-choice, .fw-pill and .fw-btn - every BUTTON in the chrome - and left every
   text field on whatever ring the browser felt like drawing, on the one control a prospect
   spends the entire session inside. The count is the deck's own arithmetic at first paint
   (forge/verticals/home-trades.js): businessName 1 + phone 1 + services (pairs, rows 4) 8 +
   towns (chips, rows 3) 3 + tagline 1 + credentials 3 + process (pairs, rows 3) 6 + faq
   (pairs, rows 4) 8 + reviews (quotes, rows 2 x textarea+input) 4 + offer 1 = 36, and
   chips/pairs/quotes append a row as you type, so 36 is the floor and not the total.
   The three anchors are NOT in this rule and never were part of the defect: styles.css:957
   is a bare `a:focus-visible` at (0,1,1) that /build loads, so .fw-home and both .fw-link
   exits are already rung by the site sheet. Only input and textarea are naked, because that
   list names .btn and summary and no field tag.

   KEYED ON THE TAG, NOT ON A CLASS, and that is the whole defect: wizard.js builds these
   with createElement('input') / createElement('textarea') and never sets a className, so a
   class-keyed rule cannot see them - which is also exactly why the class-keyed set-cover in
   wizard-chrome.test.js passed while 36 controls had none. Scoped to .forge-wizard rather
   than to .fw-input so a field added anywhere in the chrome later is rung on arrival.
   `select` and `summary` are deliberately NOT listed: neither exists in the chrome, and a
   rule matching nothing is a rule nobody maintains - the model-driven cover in
   wizard-chrome.test.js is what makes one loud the day it appears.

   THE SAME RING THE OTHER THREE GOT: 2px solid --accent at offset 2px, byte-identical to
   .fw-btn and .fw-pill, so the chrome reads as one system. --accent against --body-bg is
   10.73 / 7.26 / 6.70 on amber / green / USA (recomputed with forge/tokens.js
   contrastRatio()), the field's background is `transparent` so the ring sits on the page
   rather than on a fill, and no rule in this file puts an opacity over a field, so that is
   what it measures at. border-radius is inherited from the field's own 8px, as at .fw-btn.
   .fw-choice's 3px offset was REJECTED on arithmetic: the ring's footprint is offset +
   width, and .fw-field's caption-to-input gap is 4px - 2+2 fits it exactly, 3+2 would put
   the ring through the caption above it. A second inner halo in --accent-ink (what the
   generated page does with --focus-halo) was REJECTED for the reason already recorded at
   .fw-btn: the chrome has ONE ground, so the per-ground pair the page needs has nothing to
   switch on here.

   SPECIFICITY, NEVER SOURCE ORDER: (0,2,1), and it shares no element with the three (0,2,0)
   button rings - `button` is not in this selector - so no ring on this page is ever decided
   by which line came last. The file's ceiling stays (0,3,0). */
.forge-wizard input:focus-visible,
.forge-wizard textarea:focus-visible { outline: 2px solid var(--accent, currentColor); outline-offset: 2px; }
/* THE FIELD WRAPPER B10 NEEDS, AND THE TWO POSITIONAL RULES IT RETIRES.
   wizard.js wraps every control on a multi-field card in
   <label class="fw-field fw-field-key|fw-field-body"><span class="fw-field-name">Service 1
   </span><input></label>, so the caption survives the first keystroke, which a placeholder
   does not. min-width:0 is load-bearing, not tidiness: .fw-field is a flex item inside
   .fw-row and without it the 38/62 split cannot shrink past the input's intrinsic width at
   390. The split is class-keyed rather than positional because the quotes row wants the WIDE
   field FIRST; wizard.js sets exactly one of the two classes on every field.
   .fw-field-name is .8rem = 12.8px, the type floor the generated page holds, and takes
   --muted-2 rather than an opacity for the reason recorded at .fw-facet-label.
   THE OLD POSITIONAL RULES ARE NARROWED, NOT DELETED. As DESCENDANT selectors they survive
   the wrap and match the input INSIDE a label, where `flex: 1 1 auto` grows along the
   label's COLUMN axis - the short author input in the quotes row would stretch to the height
   of the 2-row textarea beside it. Narrowed to `>` they match nothing once the wrap lands
   and still hold the split if it never does. Deleting them outright was REJECTED for that
   second half: this file must not break if wizard.js ships without B10. */
/* STACKED IS THE BASE, SIDE BY SIDE IS THE ENHANCEMENT (round 2, item F). Two text fields
   sharing a line is a desktop affordance; under 600px it is two half-width boxes with a
   caption over each, and the caption is the only thing telling them apart. .fw-field is
   already a column with its caption above its control, so stacking the ROW is the whole
   change and the captions come with it.
   The two width rules below live in the COMPLEMENT of this one (min-width: 600px), so no
   viewport is in both and nothing here is decided by source order - the same construction
   the 899/900 pair above the preview uses. A `flex-direction: row` override inside a
   max-width query would have tied the base rule at (0,1,0) and been resolved by which line
   came last, which this file does not allow. */
.fw-row { display: flex; flex-direction: column; gap: 12px; }
.fw-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.fw-field-name { font-size: .8rem; color: var(--muted-2, #767676); }

/* SIDE BY SIDE, AND WIDE ENOUGH FOR THE WORDS (round 2, item F).
   MEASURED FIRST, 2026-09-12, at 1280 with tools/drives/ux1.js typing the real sample:
   the key input rendered clientWidth 149 and "Water heater install" needed scrollWidth 165,
   "Emergency plumbing" 178. Both clipped, at the default font, on the services card. The
   cause is two caps multiplying: .fw-input is 46ch (396.75px measured) and the key column
   took 38% of it, which is 121px of content box for a nineteen-character service name.
   THE ARITHMETIC, so the next person can re-run it rather than re-guess it. At 1280 the
   question pane is 4fr of a 4fr/6fr grid = 512px, less clamp(20px,3vw,40px) of padding
   either side = 435px of content. A row is that, less the 10px gap, = 425px for two fields.
   "Emergency plumbing" needs 178 (150 of glyphs plus the field's 28px of padding) and the
   longest sample description, "Upfront price, no surprises", needs 215. 195 + 220 = 415 fits
   in 425 with 10px left over, which the grow ratio splits 1:2 - so the key renders 196 of
   client box against the 178 it needs, and the body 225 against 215. THAT is why the cap has
   to go as well as the split move: 38% of 397 cannot be made to hold either string, and no
   percentage of 397 holds both at once.
   WRAP, NOT A SECOND BREAKPOINT, and this is the part worth reading. The pane is 4/10 of the
   viewport on the desktop grid and the WHOLE viewport under 900px, so how wide a row is is
   not monotonic in how wide the window is: 435px at 1280, 722px at 768, 348px at 1024.
   `flex-wrap` keys the decision on the row's OWN width - when the two bases do not fit, the
   fields fall onto separate lines and each grows to the full width, which is byte for byte
   the stacked layout the base rule above draws under 600px. So a 1024px laptop gets the
   stacked form and a 768px tablet gets the side-by-side one, because that is which of them
   actually has the room. A container query would say the same thing more directly and was
   REJECTED: tests/forge/lib/cssparse.js does not list @container as a conditional at-rule,
   so every declaration inside one would vanish from the C1, C2 and B11 gates silently -
   which is a worse failure than a non-monotonic breakpoint.
   THE CAP IS RAISED, NOT REMOVED. 62ch rather than `none`: at 768 the pane is the whole
   viewport and an unbounded description field would be 690px of single-line text box. The
   class is written by wizard.js from what the card actually BUILT (a node carrying .fw-row),
   never from a list of question types - the same rule .fw-input-wide follows.
   SPECIFICITY: (0,2,0) over .fw-input's (0,1,0), so the raise is not source order.
   .fw-input-wide was the one cap still decided by source order and was lifted the same way
   on 2026-09-12 (round 3); the note is at that rule. */
@media (min-width: 600px) {
  .fw-row { flex-direction: row; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
  /* The positional pair, kept in step with the class-keyed pair below. They match nothing
     once wizard.js wraps a control in .fw-field, and exist so this sheet still lays a row
     out if it ever ships without that wrap - the reason recorded at .fw-row above. */
  .fw-row > input:first-child { flex: 1 1 195px; }
  .fw-row > input:last-child { flex: 2 1 220px; }
  /* Class-keyed, never positional: the quotes row wants its WIDE field first and the pairs
     row wants it second. wizard.js sets exactly one of the two on every field. */
  .fw-row > .fw-field-key { flex: 1 1 195px; }
  .fw-row > .fw-field-body { flex: 2 1 220px; }
  .fw-pane-question .fw-input-rows { max-width: 62ch; }
}

/* ---- the picker ---------------------------------------------------------------------
   Choice cards carry a REAL rendered mini-preview, not a colour swatch - choosing from
   something concrete is what stops the result feeling generic. */
.fw-choices {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 14px;
  max-width: none;
}
/* `color: inherit` is load-bearing, not tidiness. A <button> does not inherit colour -
   the UA gives it `buttontext`, which computed to pure black here, so the label rendered
   black on the near-black panel and the `currentColor` selected ring was invisible too.
   Both were measured in the browser at 1280px; neither is reachable from a unit test. */
/* min-height IS THE TAP TARGET, and it is stated rather than inherited from the content.
   A tile holds a scaled page shot plus two lines of text, so it measures 100px and up at
   every viewport - the audit of 2026-09-08 found no .fw-choice under 44px at 320, 375, 393,
   768 or 1280. That is exactly why the floor was missing: nothing ever brought the tile
   near it, so nobody wrote one. A target whose size is an accident of its content is one
   copy edit away from being a defect, and B9 now requires the rule rather than the luck. */
.fw-choice {
  display: flex; flex-direction: column; padding: 0; min-height: 44px;
  border: 2px solid var(--line, #444); border-radius: 10px;
  background: none; color: inherit; cursor: pointer; overflow: hidden; text-align: left;
  /* On the BASE rule, not on :hover, so the tile settles back as deliberately as it lifts.
     A transition declared only inside the hover state animates the way in and cuts the way
     out. Collapsed to none under prefers-reduced-motion at the foot of this file. */
  transition: transform 120ms ease, border-color 120ms ease;
}
.fw-choice[aria-checked="true"] { border-color: var(--accent, currentColor); box-shadow: 0 0 0 2px var(--accent, currentColor); }
.fw-choice:focus-visible { outline: 2px solid var(--accent, currentColor); outline-offset: 3px; }

/* THE SHOT: a fixed 1280px page scaled by `transform`, never by `zoom`.
   `zoom` on the page's <body> does not move the frame's VIEWPORT, and a media query is
   evaluated against the viewport - so the old tiles laid out at 187 CSS px on a 1280px
   screen AND on a 1440px one (measured over all twenty), below base-css.js's 720px grid
   breakpoint and its 1000px nav breakpoint. Twenty phone pages, at every screen width
   there is, in the card where the owner picks the shape of a desktop page.
   `transform` does not re-run layout, so the page inside is genuinely the 1280px page,
   cropped at the shot height. `--fw-shot-scale` is measured per grid in JS, because the
   tile width is whatever the grid gives it and CSS cannot divide a length by a number. */
.fw-choice-shot {
  display: block; position: relative; width: 100%;
  aspect-ratio: var(--fw-shot-w, 1280) / var(--fw-shot-h, 854);
  overflow: hidden;
  /* The placeholder a tile wears until it is scrolled to. It must NOT be white: an
     unmounted tile that paints white reads as "your page is blank", which is the one thing
     the picker must never say. */
  background: rgba(128,128,128,.14);
}
.fw-choice-frame {
  position: absolute; top: 0; left: 0;
  width: calc(var(--fw-shot-w, 1280) * 1px);
  height: calc(var(--fw-shot-h, 854) * 1px);
  border: 0;
  pointer-events: none;
  transform: scale(var(--fw-shot-scale, .16));
  transform-origin: 0 0;
}

.fw-choice-text { display: block; padding: 8px 12px 10px; }
/* THE COLOUR CHIPS. Sixteen palette tiles at 200px read as one colour, because the thing that
   differs between them is a tint the miniature spends most of its pixels NOT showing. Two discs
   carrying the palette's primary and accent are the whole signal.
   THE HAIRLINE IS LOAD-BEARING, NOT TRIM. Half the palettes are pale, and a #f4f1ea disc on a
   near-black tile with no edge is a light blob of no particular shape; --fw-edge is 6.16 / 6.08 /
   4.93 against the page, so the ring is what gives a pale chip a boundary on every theme. It is
   a box-shadow rather than a border so the ring costs no layout inside a 12px box.
   --fw-swatch is written per chip by wizard.js with style.setProperty. It never becomes a
   `style="..."` attribute in the markup, so build/index.html is still free of inline style. */
.fw-chips { display: flex; gap: 5px; margin-bottom: 5px; }
.fw-chip { display: block; width: 12px; height: 12px; border-radius: 50%;
  background: var(--fw-swatch, currentColor); box-shadow: 0 0 0 1px var(--fw-edge, #767676); }
.fw-choice-label { display: block; font-size: .88rem; font-weight: 600; }
.fw-choice-blurb { display: block; margin-top: 4px; font-size: .78rem; line-height: 1.35; opacity: .72; }

.fw-choices-note { margin: 0; font-size: .85rem; opacity: .72; max-width: 54ch; }
.fw-more { align-self: flex-start; margin-top: 4px; }

/* The two-facet filter. Twenty tiles below a 38vh preview is roughly 1,760px of scroll on
   a phone, and the most important card in the wizard is the one a trades owner with thirty
   seconds of patience is least likely to reach the bottom of. */
.fw-facets { display: flex; flex-wrap: wrap; gap: 10px 20px; margin-bottom: 2px; }
.fw-facet { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
/* THE ONLY TWO CHROME TEXT RULES THAT FAIL AA, AND ONLY ON ONE THEME. opacity:.6 on --text
   composites to 5.73 / 5.12 / 4.35 on amber / green / USA - 13.6px and 12.8px normal weight,
   so the bar is 4.5:1 and the USA theme misses it. --muted-2 at full strength is
   6.16 / 6.08 / 4.93 and lands within a few RGB points of what .6 was already painting
   (#908A7E vs #9a8f78 on amber), so this is an arithmetic fix, not a restyle.
   EVERY OTHER opacity IN THIS FILE WAS CHECKED AND PASSES: .7 -> 7.49/6.54/6.01,
   .72 -> 7.94/6.86/6.47, .75 -> 8.52/7.38/7.12, .78 -> 9.15/7.91/7.93, .8 -> 9.64/8.29/8.44.
   .fw-pill:disabled's .35 lands at 2.66/2.52/2.15 and stays: WCAG 1.4.3 exempts an inactive
   user interface component, and dimming IS the disabled cue. */
.fw-facet-label { font-size: .8rem; color: var(--muted-2, #767676); margin-right: 2px; }
.fw-pill {
  min-height: 38px; padding: 0 14px; font: inherit; font-size: .85rem; cursor: pointer;
  border: 1px solid var(--fw-edge, #767676); border-radius: 999px;
  background: none; color: inherit;
  transition: transform 120ms ease, border-color 120ms ease;
}
.fw-pill[aria-checked="true"] {
  border-color: var(--accent, currentColor);
  box-shadow: inset 0 0 0 1px var(--accent, currentColor);
  font-weight: 700;
}
.fw-pill:disabled { opacity: .35; cursor: default; }
.fw-pill:focus-visible { outline: 2px solid var(--accent, currentColor); outline-offset: 2px; }

/* ---- the done screen ------------------------------------------------------------------
   The template card sits at deck index 3, where the page is a hero and a footer, so band
   rhythm is genuinely not on screen when the biggest choice in the wizard is made. This is
   the one moment in the flow when the FINISHED page can be shown in another shape. */
.fw-repick { margin-top: 26px; }
/* ONE RULE, TWO HEADINGS (round 3, item E). The done screen now carries two sections and
   they are peers - "Here it is in four other shapes." and "Change an answer." - so they are
   one typographic object rather than two rules that agree today. A selector LIST is read by
   tests/forge/lib/cssparse.js as one rule per selector, so both halves are measured
   separately by every gate in wizard-chrome.test.js and neither can hide behind the other. */
.fw-repick-head, .fw-edits-head, .fw-save-head, .fw-quote-head { margin: 0 0 4px; font-size: 1.05rem; }
.fw-repick-hint, .fw-edits-hint { margin: 0 0 12px; font-size: .85rem; opacity: .72; max-width: 54ch; }
.fw-repick .fw-choices { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); margin-bottom: 12px; }

/* ---- change an answer (round 3, item E) -------------------------------------------------
   A ROW IS A TWO-COLUMN GRID: the question over its answer on the left, the button spanning
   both of those rows on the right. ONE LAYOUT AT EVERY WIDTH, and no media query, which is
   the whole reason it is a grid.
   FLEX WAS BUILT FIRST AND SHOT, so this is measured rather than preferred. With three flex
   items and `flex-wrap`, 393 put the prompt and the summary on line one and dropped the Edit
   button onto a line of its own, and the middle widths (900 to ~1050, where the question pane
   is 4/10 of the viewport and at its narrowest) did the same - a 44px control floating under
   the pair it belongs to, eighteen times down the page. Fixing that with a breakpoint means
   picking a width for a row whose own width is not monotonic in the viewport's, which is the
   trap recorded at .fw-row. A grid row cannot produce that state at all: the button has a
   cell, and the cell is beside the text at every width there is.
   AND THE SUMMARY GAINS THE WHOLE COLUMN. minmax(0, 1fr) rather than 1fr is load-bearing -
   a grid track's default minimum is min-content, so the ellipsis below would never fire and
   a long offer would widen the row instead. */
.fw-edits { margin-top: 26px; }
.fw-edit-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.fw-edit { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center;
           column-gap: 12px; row-gap: 2px; }
.fw-edit-q { grid-column: 1; min-width: 0; font-size: .92rem; }
/* THE SUMMARY IS ONE LINE, AND IT IS CLIPPED RATHER THAN SHORTENED IN JS. A tagline or an
   offer can be a sentence, and a JS truncation would put an ellipsis into the accessible
   name as well as into the picture; clipping in CSS leaves the full string in the DOM for a
   screen reader and for select-and-copy, and the Edit button beside it is how the whole
   answer is reached anyway.
   --muted-2 at full strength rather than an opacity: 6.16 / 6.08 / 4.93 on amber / green /
   USA, the arithmetic recorded at .fw-facet-label. The prompt keeps --text, so the question
   and the answer are told apart by weight of colour rather than by a rule or an indent. */
.fw-edit-a { grid-column: 1; min-width: 0; font-size: .86rem; color: var(--muted-2, #767676);
             overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* (0,2,0), NEVER (0,1,0). .fw-btn sets `padding` and `font` (which is a font-size), so a
   bare .fw-edit-btn rule would tie it at equal specificity on both properties and the winner
   would be whichever line came last - which this file does not allow. Scoping to the row
   costs nothing and settles it by specificity. The 44px floor is NOT restated: it comes from
   .fw-btn's own unconditional min-height, which is what B9 measures.
   `1 / span 2` rather than `1 / 3`: a row whose prompt wraps is still two GRID rows, and a
   line number would be a claim about how many lines the question takes. */
.fw-edit .fw-edit-btn { grid-column: 2; grid-row: 1 / span 2; padding: 0 14px; font-size: .86rem; }

/* THE TWO-STEP START OVER. The confirm REPLACES the control, so nothing below it moves and
   there is no dialog to trap focus in; wizard.js toggles `hidden` on the two halves and
   moves focus to "Keep it". Both halves live in this one row, so the paragraph keeps its
   height either way and the done screen does not jump under the prospect's hand. */
.fw-restart { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.fw-confirm { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
/* AN AUTHORED `display` BEATS THE UA'S `[hidden] { display: none }`, AND THAT IS A DEFECT
   THIS FILE SHIPPED FOR TWENTY MINUTES. The UA rule is (0,1,0) and `.fw-confirm` is (0,1,0)
   too, so the later sheet wins - the confirm was drawn on the done screen at all times, and
   tools/axtree.mjs caught it as two extra tab stops reading "Yes, start over" and "Keep it"
   before anybody saw the pixels. `hidden` is the only mechanism wizard.js uses to show and
   hide anything, so every element in this sheet that declares a `display` must restore it;
   the gate in tests/forge/wizard-chrome.test.js now derives that set rather than listing it.
   (0,2,0) over the rule above, so it is specificity and not source order. */
.fw-confirm[hidden] { display: none; }
.fw-confirm-ask { font-weight: 700; }

/* ---- part B: save the build, get a code, ask for a quote --------------------------------
   Three panels on the done screen and one disclosure on card 1. Everything here obeys the
   same four laws the rest of the file does and the notes only record where one of them
   actually decided something.

   EVERY HIDEABLE REGION HERE CARRIES ITS OWN `[hidden]` COMPANION. The UA sheet's
   `[hidden] { display: none }` is (0,1,0), so a bare `.fw-code { display: flex }` outranks
   nothing and ties it - and a tie is resolved by which sheet came last, which means the
   panel is drawn on the done screen from first paint with its code face empty. That is the
   defect .fw-confirm shipped for twenty minutes above; the gate in
   tests/forge/wizard-chrome.test.js derives the set from this file and the markup rather
   than holding a list. The companions are (0,2,0), so it is specificity and not order. */
.fw-save { display: flex; flex-direction: column; gap: 10px; margin-top: 26px; }
.fw-save[hidden] { display: none; }
.fw-save-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 4px 0 0; }
/* THE WIDGET SLOT, AND IT IS ON ITS OWN LINE RATHER THAN BESIDE THE BUTTON. A `flexible`
   Turnstile widget is 100% wide with a 300px floor
   (developers.cloudflare.com/turnstile/get-started/client-side-rendering/widget-configurations/),
   and the question pane's content box is 337px at a 393px viewport - so a 300px widget
   sharing a row with a 170px button would push the row 130px past the edge the first time a
   visitor was actually challenged. The cap keeps it from spanning a 1280px pane as well. */
.fw-check { max-width: 320px; min-width: 0; }
/* ONE STATUS LINE, THREE PLACES, AND IT IS NOT PAINTED RED. styles.css declares --error only
   on :root and it measures 2.27:1 against the page on the USA theme, which is the same
   arithmetic that kept the destructive Start over button off it. Full-strength --text at 700
   is what carries "this did not work" here, exactly as the confirm's own question does.
   No `display` is authored on it, so the UA's [hidden] rule governs it unopposed and it
   needs no companion. */
.fw-say { margin: 0; font-size: .88rem; font-weight: 700; max-width: 54ch; }

/* THE CODE PANEL. The face is the display token every heading on /build already uses and the
   same clamp() as .fw-prompt: this string IS the payoff of the screen, and it is the one
   thing the prospect has to read back to us correctly. --font-display resolves to VT323 with
   a monospace fallback (styles.css:28); /build loads no Google Fonts link of its own, so what
   renders today is the monospace tail - which is the right typographic object for a code
   either way, and the day the page loads the family it becomes VT323 with no edit here.
   `overflow-wrap: anywhere` on the link, not `break-word`: the URL is 52 characters with no
   space in it and the panel is 305px wide at 393, so a rule that only breaks BETWEEN words
   would let it run off the side of the card. */
.fw-code { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; padding: 16px;
  border: 1px solid var(--fw-edge, #767676); border-radius: 10px; }
.fw-code[hidden] { display: none; }
.fw-code-note { margin: 0; font-size: .88rem; }
.fw-code-face { margin: 0; font-family: var(--font-display); letter-spacing: var(--display-tracking);
  font-size: clamp(1.5rem, 3vw, 2.1rem); line-height: 1.15; overflow-wrap: anywhere; }
.fw-code-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 0; }
.fw-code-link-head { margin: 6px 0 0; font-size: .85rem; color: var(--muted-2, #767676); }
.fw-code-link { font-size: .9rem; min-width: 0; overflow-wrap: anywhere; }
.fw-code-life, .fw-save-note { margin: 2px 0 0; font-size: .85rem; color: var(--muted-2, #767676); max-width: 54ch; }

/* REQUEST A QUOTE. The fields are the wizard's own: .fw-field wraps every one of them with a
   real caption, which is the same shape the six multi-field cards use, so the quote form is
   not a second field idiom on the same page. The honeypot is deliberately NOT inside a
   .fw-field - see the note at the field rule near the top of this file. */
/* NO SEPARATOR RULE, AND C1 IS WHY. A `border-top: 1px solid var(--line)` shipped here for ten
   minutes and the chrome's own contrast gate caught it at 1.32:1 against the page on amber:
   --line is rgba(hue,.16) and cannot be a component boundary on a dark theme. The only token
   that clears 3:1 is --fw-edge at 6.16 / 6.08 / 4.93, which is a loud rule for a quiet division -
   the same arithmetic, and the same verdict, that round 2 reached at .fw-scope. Air does it
   instead: the code panel above already draws its own box, so 26px of space is the division. */
.fw-quote { display: flex; flex-direction: column; gap: 12px; margin-top: 26px; }
.fw-quote[hidden] { display: none; }
.fw-quote-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 2px 0 0; }
.fw-quote-done { margin: 22px 0 0; font-size: .95rem; font-weight: 700; max-width: 54ch; }
.fw-opened { margin: 0 0 12px; font-size: .85rem; color: var(--muted-2, #767676); }

/* HAVE A CODE? on card 1. The disclosure button stays on screen when the panel opens rather
   than being replaced by it: `aria-expanded` is the state, and a control that vanishes when
   you press it gives a screen reader nothing to report the new state on.
   The panel's fields are capped at the same 46ch .fw-input uses, so the code box lines up
   with the business-name box above it instead of running the width of the pane. */
.fw-recall { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; }
.fw-recall[hidden] { display: none; }
.fw-recall-panel { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px;
  width: 100%; max-width: 46ch; }
.fw-recall-panel[hidden] { display: none; }
.fw-recall-field { flex: 1 1 180px; }
/* THE BUSY MARK, AND THE WORDS NEXT TO IT ARE WHAT ACTUALLY SAY SO. Under reduced motion the
   square stops turning and "Opening your build" is still on the line, so the state is carried
   by the sentence and the animation is decoration on top of it - which is the only shape a
   loading indicator may take in a file whose motion all collapses on request.
   A rotating square rather than a fading one: a rule declaring `opacity` is a rule the C2
   contrast gate measures as text, and it would be right to. */
.fw-spin { display: inline-block; width: 9px; height: 9px; margin-left: 8px; border-radius: 2px;
  background: var(--accent, currentColor); animation: fw-spin 900ms linear infinite; }
.fw-spin[hidden] { display: none; }
@keyframes fw-spin { from { transform: rotate(0deg); } to { transform: rotate(180deg); } }

.fw-actions { display: flex; gap: 10px; align-items: center; }
/* The primary button borrows the site's own accent pair rather than `currentColor` over
   `canvas`. `canvas` is the UA canvas, which is WHITE on all three themes, so a filled
   button rendered white-on-white with an invisible label - caught in the 1280px shell
   screenshot, and no assertion would have found it. --accent-ink is the token that is
   already contrast-checked against --accent in every theme. */
.fw-btn { min-height: 44px; padding: 0 22px; font: inherit; font-weight: 600; cursor: pointer;
  border: 1px solid var(--accent, currentColor); border-radius: 8px;
  background: var(--accent, currentColor); color: var(--accent-ink, #111);
  transition: transform 120ms ease, border-color 120ms ease; }
.fw-btn-ghost { background: none; color: inherit; border-color: currentColor; opacity: .8; }
/* ONE SELECTOR COVERS ALL FIVE. Back, Skip, Next, Start over and Show all twenty are the
   five most-used controls on the page and every one of them carries `fw-btn` - the four in
   the markup (index.html:83, 86, 90, 91, 92) and the one built in JS
   (wizard.js, className 'fw-btn fw-btn-ghost fw-more' - cited by string, not by line, because
   that file moves). They had no authored focus style and were
   taking whatever ring the browser felt like drawing.
   Deliberately identical to the site's own ring at styles.css:956-965 (2px solid var(--accent),
   offset 2px) rather than to .fw-choice's 3px offset: .btn on the rest of the site is also an
   accent-FILLED control and ships with exactly this, so a second inner ring in --accent-ink
   was REJECTED as a pattern this site does not use. --accent against --body-bg is
   10.73 / 7.26 / 6.70, and through .fw-btn-ghost's opacity:.8 it is still 7.10 / 5.01 / 4.78 -
   over 3:1 on every theme in both variants. border-radius is inherited by the outline from
   .fw-btn's own 8px, so no radius is restated. */
.fw-btn:focus-visible { outline: 2px solid var(--accent, currentColor); outline-offset: 2px; }
.fw-actions .fw-btn-ghost:last-of-type { margin-right: auto; }
/* WAS `#forge-next`, WHICH IS (1,0,0) IN A FILE THAT DECLARES A (0,3,0) CEILING. The one
   rule keyed on an id was the one rule the ceiling did not cover, and a ceiling with an
   exception in it is a claim rather than a rule. (0,1,0), and no other rule in this sheet
   sets margin-left on this element, so the move introduces no tie either. wizard.js still
   reaches the button by id - the class is for the stylesheet, not for the script. */
.fw-btn-next { margin-left: auto; }

/* THE EMPHASIS, AND IT IS NOT A DISABLED STATE IN DISGUISE (round 2, item D).
   Every card in the deck is skippable and Next never validates, so the button must never be
   `disabled` and Skip is never touched. What this says is narrower and true: the current
   card HAS an answer, so there is something to carry forward. wizard.js adds and removes the
   class from nextEmphasis() on every render and on every keystroke.
   --btn-bg IS THE SITE OWN CTA FILL, not a new colour: styles.css declares it per theme
   beside --accent, and the emphasised Next is therefore the same object the rest of
   valortechsystems.com calls a primary button. The label stays --accent-ink, which measures
   10.14 / 9.03 / 5.84 against the gradient first stop and 6.52 / 5.08 / 7.70 against its
   last, on amber / green / USA - AA on all six, computed with forge/tokens.js
   contrastRatio() and asserted in tests/forge/wizard-chrome.test.js.
   REJECTED: `box-shadow: var(--btn-shadow)`. It is the matching token, but a 30px blur at
   0.55 alpha paints nothing like the colour it declares, so the C1 gate would be measuring a
   value that is never on screen - a rule passing for the wrong reason.
   SPECIFICITY: .fw-actions .fw-btn-go is (0,2,0) and .fw-btn is (0,1,0), so the fill is
   decided by specificity. At (0,1,0) the two would have tied on `background` and the winner
   would have been whichever line came last. */
.fw-actions .fw-btn-go { background: var(--btn-bg, var(--accent, currentColor)); }

/* HOVER IS A POINTER QUESTION, ASKED OF THE POINTER RATHER THAN OF THE WIDTH.
   On a touch device `:hover` STICKS after a tap - the browser leaves the last-tapped element
   in the hover state until something else is touched - so a lift and an accent edge would
   stay on a tile the prospect has already moved past. `@media (hover: hover)` asks whether
   the device can hover at all, and a phone answers no. Same reasoning, same shape, as the
   `pointer: coarse` block below.
   --accent, NOT --line-hover, and the design of record named the other one. MEASURED with
   forge/tokens.js contrastRatio() over forge/layout/composite.js over(): --line-hover
   composites to 3.39 / 2.69 / 2.56 against the page on amber / green / USA, so it FAILS the
   3:1 that WCAG 1.4.11 and this file own C1 gate ask of a component boundary on two of the
   three themes. --accent is 10.73 / 7.26 / 6.70. The same token was rejected for the field
   border at the top of this file for the same reason; this is that finding applied to hover.
   :not([aria-checked="true"]) IS LOAD-BEARING TWICE. It keeps the SELECTED tile ring
   intact - a selected tile wears the accent border plus a 2px accent box-shadow, and
   hovering it must repaint neither - and it means these rules and the two
   [aria-checked="true"] rules can never match one element at once, so a selected tile
   border-color is never decided by source order. (0,3,0), this file ceiling. */
@media (hover: hover) {
  .fw-choice:hover, .fw-pill:hover, .fw-btn:hover { transform: translateY(-1px); }
  .fw-choice:not([aria-checked="true"]):hover,
  .fw-pill:not([aria-checked="true"]):hover,
  .fw-btn-ghost:hover { border-color: var(--accent, currentColor); }
}

/* THE CARD ENTERS (round 2, item D). A new question used to appear as a hard cut: correct,
   and indistinguishable from the page having done nothing at all.
   AN ANIMATION, NOT A CLASS THAT SETS `opacity: 0`. The two draw the same picture and only
   one is safe here: a rule declaring opacity 0 is a rule wizard-chrome.test.js AA gate
   measures at 0:1, and it would be right to - it cannot know the value is transient. A
   @keyframes body is not a style rule, so the gate never sees it and never has to carry an
   exception for it. The same reasoning is already recorded at updateEmpty() in wizard.js.
   `both` fill mode, so the card holds the first frame until the animation starts rather than
   flashing the finished state for one frame first.
   THE LIVE REGION IS NOT DELAYED. #forge-say text is written synchronously by renderCard
   before this class goes on, and the animation moves pixels an assistive technology never
   reads - so the announcement happens on the same tick it always did.
   THE PAYOFF FOLLOWS THE CARD BY 60ms. It is the line that says what this answer will do to
   the page, and it lands just after the question rather than with it. (0,2,0), and it is the
   only rule in this file that animates .fw-payoff, so nothing ties. */
@keyframes fw-card-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
.fw-enter { animation: fw-card-in 160ms ease-out both; }
.fw-enter .fw-payoff { animation: fw-card-in 160ms ease-out 60ms both; }

.fw-pane-preview { display: flex; flex-direction: column; gap: 8px; padding: 28px 20px; min-height: 0; }
/* ONE ROW: what the pane is, and at which width it is being drawn. The label keeps
   `margin-right: auto` rather than the row using space-between, so a third control added
   later lands beside the toggle instead of at the far end of the pane. */
.fw-preview-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
/* --muted-2 rather than opacity:.6 - the arithmetic is in the note at .fw-facet-label. */
.fw-preview-label { margin: 0 auto 0 0; font-size: .85rem; color: var(--muted-2, #767676); }
/* The label is wrong at Done, and the [data-state] hook fixes it with no extra JS. */
.fw-label-done { display: none; }
.forge-wizard[data-state="done"] .fw-label-asking { display: none; }
.forge-wizard[data-state="done"] .fw-label-done { display: inline; }

/* THE DESKTOP / PHONE TOGGLE, in this file's existing pill idiom and introducing no new
   visual language. The selected state is .fw-pill's own [aria-checked="true"] treatment -
   accent border, 1px inset accent ring, bold - which until now was a rule with nothing to
   match, because the facet bar signals itself with aria-pressed.
   ONE CAPSULE WITH TWO BORDERLESS HALVES WAS REJECTED, on this file's own law rather than on
   taste: it needs `.fw-views .fw-pill { border: 0 }` at (0,2,0), which TIES
   `.fw-pill[aria-checked="true"]`'s border-color at (0,2,0) - and a tie is decided by source
   order. Two pills with a 6px gap is the same object read the same way, at (0,1,0).
   Every other requirement is inherited and not restated: the 2px --accent ring comes from
   .fw-pill:focus-visible and the 44px floor from the @media (pointer: coarse) block below. */
.fw-views { display: flex; align-items: center; gap: 6px; }
/* 44px AT EVERY WIDTH AND EVERY POINTER, not only on a coarse one. Measured 2026-09-12 with
   tools/audit.mjs at 1280 under a mouse: .fw-pill renders 38px there, because the only rules
   that raise it are the max-width:899px block and the pointer:coarse block below - so the
   mobile-audit checklist's tapTargets44 failed on both of these two buttons. The facet bar
   keeps the 38px base: it is a dense filter strip below the prompt, while these two sit in a
   header row whose other members (.fw-home's 44px line-height, .fw-btn's 44px min-height)
   are all 44, so this is the row's own height rather than a special case.
   (0,2,0), and it collides with NO other rule's min-height, so nothing here is decided by
   source order: .fw-pill[aria-checked="true"] sets border-color, box-shadow and font-weight,
   :disabled sets opacity, :focus-visible sets outline. Different properties, no tie. */
.fw-views .fw-pill { min-height: 44px; }

/* THE PREVIEW IS A REAL PAGE AT A REAL WIDTH, SCALED - never a squeezed one.
   Measured 2026-09-12: the pane is ~730px wide at a 1280px viewport, so the iframe laid the
   generated page out at 730 CSS px. base-css switches its grids to multi-column at 720px and
   shows the nav links at 1000px, so "the desktop preview" was the TABLET layout, and a
   prospect choosing a two-column template was choosing it from a picture with one. That is
   the same defect the picker tiles already carry the fix for (see THE SHOT above), and it is
   the same fix: a FIXED-width frame moved by `transform: scale()`, never `zoom`, because
   `zoom` does not move the frame's viewport and a media query is evaluated against the
   viewport.
   THE SPLIT OF DUTIES. The stage is the flow box: it takes the pane's height, carries the
   component boundary, the radius, the white placeholder ground and the mobile caps, and
   clips. The iframe is absolute inside it, so its 1280px layout width never reaches the
   page's own width. --fw-view-w is the mode (1280 or 390) and --fw-shot-scale is measured off
   the stage by the same ResizeObserver the picker grids use - one mechanism, two call sites.
   background:#fff stays on both: the generated page paints its own ground the moment srcdoc
   lands, and white is the honest placeholder for a page that is nearly always light. On the
   USA theme that white frame sat on a white page behind a 1.37:1 border, i.e. the preview had
   no edge whatsoever - which is what --fw-edge is here for. */
.fw-preview-stage {
  position: relative; overflow: hidden; flex: 1 1 auto; width: 100%; min-height: 60vh;
  border: 1px solid var(--fw-edge, #767676); border-radius: 10px; background: #fff;
}
.fw-preview {
  position: absolute; top: 0; border: 0; background: #fff;
  /* CENTRED BY ARITHMETIC, not by a flex parent, and the difference is the whole reason the
     phone mode works: the iframe's LAYOUT width is --fw-view-w while its PAINTED width is
     --fw-view-w x scale, so a flex parent would centre the 390px box and leave the picture
     inside it off to the left. */
  left: calc((100% - var(--fw-view-w, 1280) * var(--fw-shot-scale, 1) * 1px) / 2);
  width: calc(var(--fw-view-w, 1280) * 1px);
  /* As tall as the stage IN THE FRAME'S OWN COORDINATES, so scaling it fills the stage
     exactly and nothing is cropped or left blank. CSS can divide a percentage by a plain
     number, so no JS ever writes a height here. */
  height: calc(100% / var(--fw-shot-scale, 1));
  transform: scale(var(--fw-shot-scale, 1));
  transform-origin: 0 0;
}
/* PHONE MODE: the frame IS the device, so it gets a device's corners and a hairline ring.
   390 is the width Chrome's own iPhone 15 profile reports, and at every pane width above it
   the scale is 1 - so this ring is a crisp 1px rather than a scaled fraction of one.
   box-shadow rather than border: a border would grow the iframe's box by 2px and put the
   centring arithmetic above out by a pixel at every width. (0,3,0), this file's ceiling. */
.fw-pane-preview[data-view="phone"] .fw-preview {
  border-radius: 14px;
  box-shadow: 0 0 0 1px var(--fw-edge, #767676);
}
/* AND WHERE THERE IS ROOM, THE STAGE GETS OUT OF THE WAY. Shot at 1280 with the white ground
   left on, the 390px page read as a white column on a white sheet rather than as a phone -
   two nested frames, neither of them the object. Where the device FLOATS, the box around it
   stops being a boundary and the phone sits on the chrome's own ground with its hairline as
   the only edge. `transparent` rather than a colour: on the USA theme the chrome's ground IS
   white, and --fw-edge on the phone is 4.93:1 there, so the device keeps an edge either way.
   ONLY ABOVE THE BREAKPOINT, and the reason is arithmetic rather than taste: at 393 the stage
   is 369px and the phone inside it is 367, so the device does not float - the stage IS the
   device, and stripping its ground put two hairlines a pixel apart and left the blank first
   card reading as two flat bands in tools/audit.mjs's clipping proof. 900px is the exact
   complement of the max-width: 899px block below, so no width is in both or in neither.
   The white ground stays in the base rule, where it is the honest placeholder for a document
   that has not painted yet. */
@media (min-width: 900px) {
  .fw-pane-preview[data-view="phone"] .fw-preview-stage {
    background: none;
    border-color: transparent;
  }
}

/* THE EMPTY STATE, and it sits where the answer is about to land rather than in the middle
   of the pane. On the first card the page really is blank - the hero gates on the business
   name and omit-never-fake forbids a placeholder headline - so the invitation belongs in the
   chrome, over the sheet, at roughly the height the headline appears at. It fades once, the
   moment the name is non-empty, and never comes back.
   IT CARRIES THE PAGE'S OWN GROUND AND GLYPH COLOUR, not --muted-2 on nothing: the surface
   behind it is the generated page's white, where --muted-2 measures 3.19 / 3.12 / 4.93 and
   misses AA on two of the three themes (the arithmetic is in the note at --fw-edge). --text
   on --body-bg is the pair every other word in the chrome is painted with.
   The horizontal margin is the iframe's own centring arithmetic plus 16px, so in phone mode
   the caption stays inside the 390px sheet instead of spanning the whole stage.
   pointer-events: none and aria-hidden, because it is a caption over a picture and never a
   control - #forge-prompt already asks the question out loud. */
.fw-empty {
  position: absolute; top: 26%; left: 0; right: 0;
  /* A NOTE LAID ON THE SHEET, not a toolbar across it. `width: fit-content` with auto
     margins shrink-wraps the box to the sentence and centres it in the stage - which is
     where the painted page is centred too, by the identical arithmetic on .fw-preview - and
     the max-width below is what keeps it ON the phone rather than across the pane.
     WHY THE LEFT/RIGHT INSETS WENT. They were
       calc((100% - var(--fw-view-w) * var(--fw-shot-scale) * 1px) / 2 + 16px)
     on both sides, which is correct arithmetic resting on a wrong mechanism: it reads two
     custom properties that JAVASCRIPT writes onto the stage (paintView, wizard.js), so it is
     right only once wizard.js has run. Before that - first paint, or a module that failed to
     load at all - both fall back to 1280 x 1, and at a 296px stage that is
     (296 - 1280) / 2 + 16 = -476px per side. MEASURED 2026-09-12 at five widths with the two
     properties cleared: the caption sat at -8 to 328 inside a 12-to-308 stage at 320px, i.e.
     over BOTH edges of the device it is a note on, clipped by the stage's own overflow into a
     band across it. At the wider widths `width: fit-content` happened to shrink-wrap it small
     enough to stay inside, which is why the defect only showed at the narrow end - the rule
     was wrong everywhere and visibly wrong in one place.
     The rule below cannot express that state at all: 100% is the stage, which is always
     known, and `min()` takes whichever of the two bounds is tighter. Re-measured at the same
     five widths with the properties cleared, the caption does not move at all.
     min(), not a second media query: the caption has to be inside the smaller of two boxes
     that are measured in different units, and that is the arithmetic min() exists for. The
     fallback is 390 rather than 1280, so the unset case biases to the narrower device. */
  width: fit-content; margin: 0 auto;
  max-width: min(100% - 32px, calc(var(--fw-view-w, 390) * var(--fw-shot-scale, 1) * 1px - 32px));
  padding: 10px 14px;
  text-align: center; font-size: .95rem; line-height: 1.35;
  color: var(--text, inherit); background: var(--body-bg, #111);
  border: 1px solid var(--fw-edge, #767676); border-radius: 10px;
  pointer-events: none;
  transition: opacity 160ms ease;
}

/* Mobile: the preview goes ON TOP as a shrunken live pane with the question below it, so
   the growth is still visible while answering. This is the point of the format - a
   visitor who cannot see the preview while answering loses the entire reason for it. */
@media (max-width: 899px) {
  /* Rows, not just one column: the preview row sizes to its own content and the question
     row takes the slack. Left to auto rows both stretched equally, which parked 100px of
     dead space between the capped 38vh preview and the question below it. */
  .forge-wizard { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
  .fw-pane-question { border-right: 0; order: 2; padding-top: 16px; }
  .fw-pane-preview { order: 1; padding: 12px 12px 0; }
  /* The cap is on the STAGE, which is the flow box that takes the pane's height. The iframe
     inside it is absolutely positioned and 1280px (or 390px) wide, so a height cap there
     would cap nothing the prospect can see. */
  .fw-preview-stage { min-height: 38vh; max-height: 38vh; }
  /* THE CAP IS FOR ANSWERING, AND DONE IS NOT ANSWERING. 38vh is 320px of a 844px phone; it
     exists so the question and the preview are on screen at the same time, which is the whole
     point of the format. At Done there is no question, and the cap was still shrinking the one
     thing the wizard spent eighteen cards earning: 320px of a 7,026px page. Desktop already
     does the right thing here - .fw-preview is flex:1 1 auto in a 100dvh grid column, so the
     finished page fills the viewport - and this is mobile catching up.
     70vh, arithmetic not taste: 12 (pane pad) + 22 (label) + 8 (gap) + 591 (70vh of 844) + 16
     (question pad) = 649px, so the h1 "That is your site." lands at 649-682 and its reassurance
     paragraph at 683-780. Headline and payoff copy both above the fold, with 591px of finished
     page above them. 60vh was REJECTED (506px, less page, no gain); removing the cap entirely
     was REJECTED because the row is `auto` in grid-template-rows, so with no min-height the
     iframe collapses to its flex basis rather than growing.
     SPECIFICITY, NEVER SOURCE ORDER: .forge-wizard[data-state="done"] .fw-preview-stage is
     (0,3,0) and beats the (0,1,0) rule above on its own. (0,3,0) is already this file's
     ceiling (.fw-actions .fw-btn-ghost:last-of-type), so the ceiling does not rise.
     FAIL-SAFE: every rule keyed on [data-state="done"] in this file degrades to today's
     behaviour when the attribute is absent - which is the state at first paint, before
     renderCard() runs. :has() was REJECTED on this file's own precedent at .fw-input-wide.
     2026-09-12: this rule and the two label rules above were DEAD until today. Nothing in
     wizard.js ever wrote dataset.state, so the cap was never released and "Your finished
     site" was never shown - a state the CSS could express and the JS never set, which is
     exactly the defect class wizard-chrome.test.js's B12 pair was written for, and it passed
     because its JS half is skipped when no state is set at all. renderCard() sets it now. */
  .forge-wizard[data-state="done"] .fw-preview-stage { min-height: 70vh; max-height: none; }
  /* 140px, not 200: at 375px this gives two tiles side by side (the whole reason to look
     at a picker), and at 320px it degrades to one wide tile rather than two unreadable
     ones. Both were measured rather than guessed. */
  .fw-choices { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; }
  .fw-repick .fw-choices { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }
  /* A secondary control is still a tap target. */
  .fw-pill { min-height: 44px; }
}

/* THE POINTER, NOT THE WIDTH. Measured 2026-09-08 at five viewports on the template
   picker: .fw-pill renders 44px tall at 320/375/393/768 and 38px at 1280, because the only
   rule that raises it is inside the max-width:899px block above. Width is a proxy for
   pointer type and it is the wrong one - a touch laptop, a Surface, and an iPad Pro in
   landscape (1024) are all coarse-pointer devices at or above 900px, and every one of them
   gets the 38px target. `pointer: coarse` asks the question the 44px floor is actually
   about, so the floor now follows the finger rather than the viewport.

   THIS RULE ONLY EVER RAISES min-height, so it cannot fight the block above: where both
   apply they set the same 44px, and equal specificity plus equal value has no outcome to
   decide. That matters because this file's law is SPECIFICITY, NEVER SOURCE ORDER, and a
   media rule at (0,1,0) has the same specificity as the base .fw-pill rule it overrides.

   WHY B9 DID NOT CATCH IT. tests/forge/wizard-chrome.test.js:392 asks whether SOME rule in
   this file gives the element a 44px box, and a rule inside a max-width query answered yes.
   That is the B11 shape exactly - a gate keyed on a rule existing rather than on the value
   that renders - and the test is now keyed on the condition as well. */
@media (pointer: coarse) {
  .fw-pill { min-height: 44px; }
}

/* EVERY MOVING THING IN THE CHROME, COLLAPSED. The gate in
   tests/forge/wizard-chrome.test.js derives the list from this file rather than holding one
   of its own: any rule outside this block that declares `transition` or `animation` must
   have a counterpart in here under the SAME selector, and of the same kind. So a rule added
   above with no line here fails the suite - the list cannot go stale by being forgotten. */
@media (prefers-reduced-motion: reduce) {
  .fw-bar-fill { transition: none; }
  /* The empty-state caption is removed outright rather than faded. wizard.js takes the same
     branch off the same query, so the node is gone on the tick the name lands instead of
     sitting at opacity 0 for 160ms with no transition to carry it. */
  .fw-empty { transition: none; }
  /* The hover lift and the edge change. `transition: none` rather than dropping the hover
     rules: the lift and the accent edge are still drawn, they simply arrive at once. A
     visitor who asks for less motion is asking for less MOTION, not for less feedback -
     removing the affordance would take away information rather than movement. */
  .fw-choice, .fw-pill, .fw-btn { transition: none; }
  /* The card enter and the payoff behind it. With the animation gone the `both` fill mode
     goes with it, so the card paints at its natural opacity and offset on the first frame -
     which is exactly the hard cut this feature replaced, restored on request. */
  .fw-enter, .fw-enter .fw-payoff { animation: none; }
  /* The busy mark on "Have a code?". It stops turning and the line still reads "Opening your
     build", so nothing about the state is carried by the movement alone. */
  .fw-spin { animation: none; }
}
