/* widget.css — Instant Pricing widget (vanilla, no build). CN DESIGN SYSTEM v2.
 *
 * MIGRATION, NOT AN UPGRADE. The widget previously wore a --cn-ui-* token file (#238).
 * The canonical system (CNS-privately/cn-design-system v2.3.16) does not reference
 * --cn-ui- anywhere, so this is a move between two systems. Tokens now come from
 * cn-v2-tokens.css, vendored verbatim and digest-pinned.
 *
 * WHAT THIS FILE MAY NAME. Semantic tokens only — surface-*, content-*, border-*,
 * focus-*, action-*, status-*, selection-*, plus the structural scales (type-*, space-*,
 * radius-*, size-*, motion-*, stroke-*, effect-*, layout-*). NEVER primitives
 * (neutral-*, brand-*, alpha-*, chart-*): a primitive hard-codes a decision the semantic
 * layer exists to make, and the high-contrast block re-points the semantic names, not
 * the primitives — so a primitive silently opts out of high contrast.
 *
 * NO .cn-* CLASS CSS IS COPIED. The system's VERSIONING covers exports, props, tokens
 * and assets — NOT class names, which may change in a patch release. Every component
 * here is widget-owned CSS written against the system's tokens and following its
 * patterns, so a patch bump cannot restyle us by surprise.
 *
 * REASONED DIVERGENCES from the system, each deliberate:
 *   1. MONO IS FOR OPERATIONAL DATA ONLY — price, bandwidth, term, carrier count, AVR
 *      number. We do NOT adopt the system's filled-text-field-mono behaviour: a
 *      prospect's typed name, email and address stay Inter. Those are a person's own
 *      words, not machine output, and monospacing them reads as terminal echo rather
 *      than as a form the visitor is filling in.
 *   2. CHECKBOX ROWS WRAP and have no fixed width. The system's selection row is a
 *      fixed 220px; our consent labels are full sentences and would clip.
 */

/* ── Fonts ────────────────────────────────────────────────────────────────────────
   tokens.css carries FAMILY NAMES only — no @font-face and no fallback stack — so both
   are ours to supply. Static weights (the system ships no variable file): Inter 300/400/
   600 and JetBrains Mono 400/600. Inter 700 is deliberately NOT vendored: the price is
   Mono 600 (the system ships mono at 400/600 only, so 700 cannot be honoured without
   synthesis) and every other emphasis is semibold, so a 700 file would be 24KB of dead
   weight in the inlined demo. */
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 300; font-display: swap;
  src: url('fonts/inter-latin-300-normal.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('fonts/inter-latin-400-normal.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('fonts/inter-latin-600-normal.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('fonts/jetbrains-mono-latin-400-normal.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('fonts/jetbrains-mono-latin-600-normal.woff2') format('woff2'); }

/* The token is a bare family name, so every use needs a real fallback stack — without
   one a blocked or failed woff2 falls back to the UA serif and the layout shifts. */
:root {
  --w-sans: var(--cn-v2-type-family-sans), -apple-system, BlinkMacSystemFont,
            'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --w-mono: var(--cn-v2-type-family-mono), ui-monospace, SFMono-Regular, Menlo,
            Consolas, 'Liberation Mono', monospace;
  /* The shell's own measure. 560 is the widget's design width; 288 is a 320px viewport
     less two 16px gutters, which is the narrowest PR 2 will iframe it at. */
  --w-shell-max: 560px;
  /* The wide measure — see the note on .widget for why 960 and not 1200. */
  --w-shell-max-wide: 960px;
}

* { box-sizing: border-box; }
/* The [hidden] attribute must ALWAYS win. Author rules like .demo-bar{display:flex}
   otherwise beat the UA [hidden]{display:none}, which is exactly why the fixture bar
   leaked into live mode. This reset makes `hidden` authoritative for every element. */
[hidden] { display: none !important; }

html, body {
  margin: 0; background: var(--cn-v2-surface-canvas);
  font-family: var(--w-sans); color: var(--cn-v2-content-primary);
  font-size: var(--cn-v2-type-size-body-app); line-height: var(--cn-v2-type-line-body-app);
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}
body {
  padding: var(--cn-v2-layout-gutter-mobile); display: flex; flex-direction: column;
  align-items: center; gap: var(--cn-v2-space-lg);
  /* 288px of container must lay out cleanly: never let content force the page wider. */
  min-width: 0; overflow-x: hidden;
}

/* ── Fixture-mode chrome (demo only; widget.js strips it in live mode) ────────── */
.demo-watermark {
  position: fixed; top: 14px; right: -46px; transform: rotate(35deg);
  background: var(--cn-v2-surface-overlay); color: var(--cn-v2-content-primary);
  font-family: var(--w-mono); font-weight: var(--cn-v2-type-weight-semibold);
  font-size: var(--cn-v2-type-size-label); letter-spacing: var(--cn-v2-type-tracking-label);
  text-transform: var(--cn-v2-type-case-label);
  padding: var(--cn-v2-space-xs) 60px; z-index: 50;
  border-block: var(--cn-v2-stroke-hairline) solid var(--cn-v2-border-active);
  pointer-events: none;
}
.demo-bar {
  width: 100%; max-width: var(--w-shell-max); display: flex; align-items: center;
  gap: var(--cn-v2-space-md); flex-wrap: wrap;
  background: var(--cn-v2-surface-default);
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-subtle);
  border-radius: var(--cn-v2-radius-panel);
  padding: var(--cn-v2-space-md) var(--cn-v2-space-lg);
}
.demo-bar__tag {
  font-family: var(--w-mono); font-size: var(--cn-v2-type-size-label);
  letter-spacing: var(--cn-v2-type-tracking-label); text-transform: var(--cn-v2-type-case-label);
  color: var(--cn-v2-content-muted);
}
/* Fixture-only controls: 32px (size-control-small) rather than the 44px minimum. They
   never ship — widget.js removes this bar entirely in live mode — and the accessibility
   ruling exempts them, so they stay compact instead of dominating the demo page. */
.demo-bar__select, .demo-bar__btn {
  min-height: var(--cn-v2-size-control-small); font-family: var(--w-sans);
  font-size: var(--cn-v2-type-size-body-app); border-radius: var(--cn-v2-radius-control);
}
.demo-bar__select {
  flex: 1 1 240px; min-width: 0; appearance: none;
  background: var(--cn-v2-surface-raised); color: var(--cn-v2-content-primary);
  border: var(--cn-v2-stroke-hairline) solid var(--cn-v2-border-default);
  padding: 0 var(--cn-v2-space-sm); font-weight: var(--cn-v2-type-weight-light);
}
.demo-bar__btn {
  background: transparent; color: var(--cn-v2-action-ghost-content);
  border: var(--cn-v2-stroke-hairline) solid var(--cn-v2-border-default);
  padding: 0 var(--cn-v2-space-md); cursor: pointer;
  font-weight: var(--cn-v2-type-weight-semibold);
  transition: background-color var(--cn-v2-motion-duration-fast) var(--cn-v2-motion-easing-standard);
}
.demo-bar__btn:hover { background: var(--cn-v2-action-ghost-background-hover); }
.demo-bar__note { font-size: var(--cn-v2-type-size-caption); color: var(--cn-v2-content-muted); }

/* ── Widget shell ────────────────────────────────────────────────────────────── */
/* THE SHELL IS A QUERY CONTAINER. A container query, not a media query: the
   widget is embedded in an iframe (PR 2b) whose width is set by the host page and
   bears no relation to the viewport. A media query would read the iframe's own
   viewport and lay out two columns inside a 360px frame on a wide desktop. The
   container query asks the only question that matters — how much room does THIS
   widget have — so the same file is correct standalone and embedded.

   THE MEASURE. 560px is the single-column measure and stays that. Wide, the shell
   lifts to 960px: two 1fr columns inside 960 less the 24px gap and 2x24 padding
   gives ~440px each, and at the body's 14px/22px that is roughly 62 characters a
   line — inside the 45-75 that reads comfortably. 1200 was tempting and wrong:
   two 560px columns is ~78 characters, past the comfortable ceiling, and the
   price rows would stretch their label/value pairs apart with nothing between. */
.widget {
  width: 100%; max-width: var(--w-shell-max); min-width: 0;
  container-type: inline-size; container-name: widget;
  background: var(--cn-v2-surface-default);
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-subtle),
              var(--cn-v2-effect-elevation-raised);
  border-radius: var(--cn-v2-radius-card); overflow: hidden;
}
/* The shell can only BECOME wide if it is allowed to; the container query then
   decides what to do with the room. This one IS a viewport question — how much of
   the page may the widget occupy — which is a different question from how to lay
   out the width it ends up with, and that second question is the container's.
   Inside an iframe this reads the IFRAME's width, which is exactly right.

   880px is DERIVED, not chosen: the container threshold is 816px (see below) and
   the tablet gutters take 2x32, so 816 + 64 = 880 is the exact viewport at which
   the widget can first be granted the width its own query needs. The two meet with
   no dead band and no overlap — at 879 the gate is inactive, max-width stays 560
   and the container is 560; at 880 the widget is 816 and the columns split. */
@media (min-width: 880px) {
  .widget { max-width: var(--w-shell-max-wide); }
}

/* ── Two columns ──────────────────────────────────────────────────────────────
   Below 700px of CONTAINER there is no grid at all — the wrappers are plain
   blocks and the layout is exactly what it was. */
.w-cols { min-width: 0; }
.w-col-main, .w-col-side { min-width: 0; }
.w-col-side--wide { display: none; }          /* the entry explainer is wide-only */
/* ── The result screen: map + card, the contact panel beneath (CTO 2026-10-09) ───────
   NARROW (the base): the map is a ~150px strip across the TOP of the card, then the
   card; the contact panel stacks below, one field per row, a full-width button.
   WIDE (the container query below): card left, map right (380px, the mockup's column),
   from the first second of the search. The figure is in the DOM first so the strip
   comes first without any reordering; the grid areas place it on the right when wide. */
.w-result { min-width: 0; }                       /* card-only, full width: no map, no map space */
/* ONLY when the map figure rendered (.w-result--map): the strip above the card at the
   base, the right-hand column in the container query below. Base rules above the query. */
.w-result--map {
  display: grid; grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "map" "card"; gap: var(--cn-v2-space-lg);
}
.w-card { grid-area: card; min-width: 0; }
.w-map { grid-area: map; display: block; margin: 0; min-width: 0; }
/* The contact panel's company/phone pair stacks at the base (one field per row); the
   row's 180px flex-basis must not become a HEIGHT in column direction. The container
   query below restores the two-up row. ABOVE the query on purpose: equal specificity,
   source order decides. */
.w-panel .w-row { flex-direction: column; gap: 0; }
.w-panel .w-row > .w-field { flex: 0 0 auto; }
/* The map image. This base rule sits ABOVE the container query that overrides its
   height — equal specificity, so source order is what makes the wide layout win. Sized to the column
   (452px at 960 wide; the 2x image scales down, never up), on the raised surface with
   the panel radius and the hairline the other panels use. */
.w-map__img {
  display: block; width: 100%; height: 150px;      /* the narrow strip; the wide column overrides */
  object-fit: cover; border-radius: var(--cn-v2-radius-panel);
  background: var(--cn-v2-surface-raised);
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-subtle);
}

/* 816px is DERIVED from the ruled 380px minimum column, not picked:
       2 x 380 + 24 (the space-xl gap) + 32 (the body's space-lg padding) = 816.
   At the previous 700 each column was ~340px and the company/phone pair inside
   .w-row shrank to ~164px each — flex-basis 180 shrinks below its basis, so they
   stayed side by side and got cramped rather than wrapping. At 816 the columns are
   380 and that pair is 184 each. Verified against the measured 960 case: columns
   there are (960-32-24)/2 = 452, which is what the browser reports. */
@container widget (min-width: 816px) {
  .w-cols {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--cn-v2-space-xl);
    align-items: start;
  }
  /* Placed EXPLICITLY, not by source order: the source order is the narrow
     reading order and must not be disturbed to suit the wide one.
     grid-ROW matters as much as grid-column here. With the column alone, an item
     whose column has already been passed by the auto-placement cursor is pushed to
     the NEXT ROW — so on the priced screen, where the side column comes first in
     the DOM, the form landed underneath the prices instead of beside them. Pinning
     both to row 1 makes placement independent of source order, which is the whole
     point of placing them explicitly. */
  .w-col-main { grid-column: 1; grid-row: 1; }
  .w-col-side {
    grid-column: 2; grid-row: 1;
    /* Sticky so the price stays in view while the form is filled. The offset is
       the body padding, so it sits flush with the top of the panel. */
    position: sticky; top: var(--cn-v2-space-lg);
  }
  .w-col-side--wide { display: block; }
  /* The result screen goes two-column: the card, then the map in a 380px column that
     runs the card's full height. */
  .w-result--map {
    grid-template-columns: minmax(0, 1fr) 380px; grid-template-areas: "card map";
    gap: var(--cn-v2-space-xl); align-items: stretch;
  }
  .w-map__img { height: 100%; min-height: 360px; }
  /* The contact panel keeps its two-up company/phone row only here. */
  .w-panel .w-row { flex-direction: row; gap: var(--cn-v2-space-md); }
  .w-panel .w-row > .w-field { flex: 1 1 180px; }
}

/* The wide-layout entry explainer. Reads as guidance, not as a claim. */
.w-aside {
  background: var(--cn-v2-surface-raised);
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-subtle);
  border-radius: var(--cn-v2-radius-panel);
  padding: var(--cn-v2-space-lg);
}
.w-aside .w-h3 { margin-top: 0; }
.w-steps {
  margin: 0; padding-left: var(--cn-v2-space-lg);
  color: var(--cn-v2-content-secondary);
  font-size: var(--cn-v2-type-size-body-app); line-height: var(--cn-v2-type-line-body-app);
}
.w-steps li + li { margin-top: var(--cn-v2-space-sm); }
.widget__body { padding: var(--cn-v2-space-lg); }

/* ── Typography (system scale only; the retired 15px floor is gone) ───────────── */
.w-h2 {
  font-size: var(--cn-v2-type-size-heading); line-height: var(--cn-v2-type-line-heading);
  letter-spacing: var(--cn-v2-type-tracking-heading);
  font-weight: var(--cn-v2-type-weight-semibold); color: var(--cn-v2-content-strong);
  margin: 0 0 var(--cn-v2-space-sm);
}
.w-h3 {
  font-size: var(--cn-v2-type-size-subheading); line-height: var(--cn-v2-type-line-subheading);
  letter-spacing: var(--cn-v2-type-tracking-subheading);
  font-weight: var(--cn-v2-type-weight-semibold); color: var(--cn-v2-content-strong);
  margin: 0 0 var(--cn-v2-space-sm);
}
.w-sub {
  font-size: var(--cn-v2-type-size-body-app); line-height: var(--cn-v2-type-line-body-app);
  color: var(--cn-v2-content-secondary); margin: 0 0 var(--cn-v2-space-lg);
}
.w-muted {
  font-size: var(--cn-v2-type-size-body-app); line-height: var(--cn-v2-type-line-body-app);
  color: var(--cn-v2-content-muted);
}
/* Divergence 1: the address is the PROSPECT's own text, so it stays Inter. */
.w-addr {
  font-size: var(--cn-v2-type-size-body-app); line-height: var(--cn-v2-type-line-body-app);
  color: var(--cn-v2-content-secondary); margin: 0 0 var(--cn-v2-space-lg);
  overflow-wrap: break-word;
}

/* ── Fields ──────────────────────────────────────────────────────────────────── */
.w-field { margin-bottom: var(--cn-v2-space-lg); min-width: 0; }
/* Labels are INTERFACE text: Inter, semibold, sentence case — not mono, not uppercase,
   no tracking. The system's uppercase label treatment is for eyebrow/meta labels. */
.w-label {
  display: block; margin-bottom: var(--cn-v2-space-sm); font-family: var(--w-sans);
  font-size: var(--cn-v2-type-size-body-app); line-height: var(--cn-v2-type-line-body-app);
  font-weight: var(--cn-v2-type-weight-semibold); color: var(--cn-v2-content-secondary);
}
.w-input, .w-select, .w-static {
  width: 100%; min-width: 0; appearance: none;
  background: var(--cn-v2-surface-default); color: var(--cn-v2-content-strong);
  /* An INSET hairline, per the system: a real border would grow the 44px box and the
     focus ring then has to fight it for the same pixel. */
  border: 0; box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-default);
  border-radius: var(--cn-v2-radius-control);
  padding: 0 var(--cn-v2-space-md); min-height: var(--cn-v2-size-control-standard);
  font-family: var(--w-sans); font-size: var(--cn-v2-type-size-body-app);
  line-height: var(--cn-v2-type-line-body-app); font-weight: var(--cn-v2-type-weight-light);
  transition: background-color var(--cn-v2-motion-duration-fast) var(--cn-v2-motion-easing-standard),
              box-shadow var(--cn-v2-motion-duration-fast) var(--cn-v2-motion-easing-standard);
}
.w-input:hover, .w-select:hover { background: var(--cn-v2-surface-hover); }
.w-input::placeholder { color: var(--cn-v2-content-muted); opacity: 1; }
.w-input[aria-invalid="true"] {
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-status-critical);
}
/* Select needs room for the chevron; the chevron is a background image so it survives
   `appearance: none` and needs no extra element in the JS-emitted markup. */
.w-select {
  padding-right: var(--cn-v2-size-control-large);
  background-image: linear-gradient(45deg, transparent 50%, var(--cn-v2-content-secondary) 50%),
                    linear-gradient(135deg, var(--cn-v2-content-secondary) 50%, transparent 50%);
  background-position: calc(100% - 22px) calc(50% + 2px), calc(100% - 16px) calc(50% + 2px);
  background-size: 6px 6px, 6px 6px; background-repeat: no-repeat;
}
.w-static { color: var(--cn-v2-content-secondary); display: flex; align-items: center; }
/* Wrap, don't squeeze: at 288px two 160px fields cannot share a row. */
.w-row { display: flex; gap: var(--cn-v2-space-md); flex-wrap: wrap; }
.w-row > .w-field { flex: 1 1 180px; min-width: 0; }

/* ── Checkbox — hidden native input + a custom 20px mark ──────────────────────── */
/* Divergence 2: the row WRAPS and has no fixed width (the system's is a fixed 220px);
   our consent copy is a full sentence. The row still meets the 44px target: the label
   is the click target, is at least 44px tall, and its 20px mark is centred within it. */
.w-check {
  display: flex; align-items: center;
  min-height: var(--cn-v2-size-control-standard);
  padding-block: var(--cn-v2-space-md); margin-bottom: var(--cn-v2-space-sm);
  font-size: var(--cn-v2-type-size-body-app); line-height: var(--cn-v2-type-line-body-app);
  color: var(--cn-v2-content-secondary); cursor: pointer;
}
/* The span is the row: the mark is a FLEX ITEM, not an inline ::before, so a consent
   sentence that wraps indents beside the mark instead of flowing underneath it. The
   markup is label > input + span and JS is not changing, so the mark has to live on the
   span — which is why the span, not the label, is the flex container. */
.w-check > span {
  display: flex; align-items: flex-start; gap: var(--cn-v2-space-md); min-width: 0;
}
/* The native input is visually hidden but still focusable and still the a11y source of
   truth — never display:none, which removes it from the tab order and the a11y tree. */
.w-check input[type="checkbox"] {
  position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0;
}
.w-check input[type="checkbox"] + span::before {
  content: ""; flex: 0 0 auto;
  width: var(--cn-v2-size-icon-medium); height: var(--cn-v2-size-icon-medium);
  border-radius: var(--cn-v2-radius-control); background: var(--cn-v2-surface-default);
  outline: var(--cn-v2-stroke-hairline) solid var(--cn-v2-border-default);
  outline-offset: 0; margin-top: 1px;
  transition: background-color var(--cn-v2-motion-duration-fast) var(--cn-v2-motion-easing-standard);
}
/* Checked: primary fill plus a TICK — never colour alone (see the status rule below). */
.w-check input[type="checkbox"]:checked + span::before {
  background: var(--cn-v2-action-primary-background);
  outline-color: var(--cn-v2-border-default);
  content: "\2713"; color: var(--cn-v2-action-primary-content);
  font-size: var(--cn-v2-type-size-caption); line-height: var(--cn-v2-size-icon-medium);
  text-align: center; font-weight: var(--cn-v2-type-weight-semibold);
}
.w-check input[type="checkbox"]:hover + span::before {
  outline: var(--cn-v2-stroke-focus) solid var(--cn-v2-focus-halo);
}
.w-check input[type="checkbox"]:focus-visible + span::before {
  outline: var(--cn-v2-stroke-focus) solid var(--cn-v2-focus-ring);
}
/* An invalid checkbox (consent left unticked) rings its MARK in critical, as an invalid
   text field rings its edge — the same treatment for every field error. The cue is not
   colour alone: the linked message beneath carries the "!" glyph and the text. When the
   mark is :focus-visible the focus ring wins (WCAG 2.4.7) — which is the right split:
   a KEYBOARD user, returned here by the re-render, needs to see where focus went; a
   POINTER user's programmatic focus is not :focus-visible, so they see the error ring.
   Both 2px outlines on one mark cannot show at once, and the message covers the error. */
.w-check input[type="checkbox"][aria-invalid="true"] + span::before {
  outline: var(--cn-v2-stroke-focus) solid var(--cn-v2-status-critical);
}
.w-check input[type="checkbox"][aria-invalid="true"]:focus-visible + span::before {
  outline-color: var(--cn-v2-focus-ring);
}

/* ── Address autocomplete ────────────────────────────────────────────────────── */
.w-address-wrap { position: relative; min-width: 0; }
.w-predictions {
  position: absolute; left: 0; right: 0; top: calc(100% + var(--cn-v2-space-xs)); z-index: 20;
  background: var(--cn-v2-surface-overlay);
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-default),
              var(--cn-v2-effect-elevation-overlay);
  border-radius: var(--cn-v2-radius-panel); overflow: hidden;
}
/* 44px minimum: a suggestion is a primary touch target on a phone and was ~38px. */
.w-prediction {
  display: flex; align-items: center; width: 100%; text-align: left;
  background: transparent; border: 0; color: var(--cn-v2-content-primary);
  min-height: var(--cn-v2-size-control-standard);
  padding: var(--cn-v2-space-sm) var(--cn-v2-space-md);
  font-family: var(--w-sans); font-size: var(--cn-v2-type-size-body-app);
  line-height: var(--cn-v2-type-line-body-app); cursor: pointer;
}
.w-prediction:hover { background: var(--cn-v2-surface-hover); }
.w-prediction[aria-selected="true"] { background: var(--cn-v2-selection-background); }

/* ── Buttons — primary and ghost only ────────────────────────────────────────── */
/* Sentence case, no tracking, and HOVER CHANGES THE BACKGROUND. The v3 lift-and-brighten
   is retired: a 2px translate on press-and-hold made the CTA feel like it had moved away
   from the finger, and brightness() fabricates a colour the token set does not contain. */
.w-btn {
  width: 100%; display: inline-flex; align-items: center; justify-content: center;
  gap: var(--cn-v2-space-sm);
  min-height: var(--cn-v2-size-control-standard); min-width: 106px;
  padding: 0 var(--cn-v2-space-lg); border: 0;
  border-radius: var(--cn-v2-radius-control);
  background: var(--cn-v2-action-primary-background); color: var(--cn-v2-action-primary-content);
  font-family: var(--w-sans); font-size: var(--cn-v2-type-size-body-app);
  line-height: var(--cn-v2-type-line-body-app); font-weight: var(--cn-v2-type-weight-semibold);
  letter-spacing: normal; text-transform: none; cursor: pointer;
  transition: background-color var(--cn-v2-motion-duration-fast) var(--cn-v2-motion-easing-standard);
}
.w-btn:hover:not(:disabled) { background: var(--cn-v2-action-primary-background-hover); }
.w-btn:active:not(:disabled) { opacity: 0.82; }
.w-btn:disabled {
  background: var(--cn-v2-action-disabled-background);
  color: var(--cn-v2-action-disabled-content); cursor: not-allowed;
}
.w-btn--ghost {
  background: transparent; color: var(--cn-v2-action-ghost-content);
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-default);
}
.w-btn--ghost:hover:not(:disabled) { background: var(--cn-v2-action-ghost-background-hover); }

/* Turnstile block — LIVE wraps the real .cf-turnstile; fixture holds the faux checkbox. */
.w-turnstile {
  display: flex; align-items: center; gap: var(--cn-v2-space-md); flex-wrap: wrap;
  background: var(--cn-v2-surface-raised);
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-subtle);
  border-radius: var(--cn-v2-radius-panel);
  min-height: var(--cn-v2-size-control-standard);
  padding: var(--cn-v2-space-sm) var(--cn-v2-space-md);
  margin-bottom: var(--cn-v2-space-lg);
  font-size: var(--cn-v2-type-size-body-app); color: var(--cn-v2-content-secondary);
}
.w-turnstile input { accent-color: var(--cn-v2-action-primary-background); width: 20px; height: 20px; }
/* LIVE: interaction-only hides the Turnstile iframe until a challenge is needed, so the
   wrapper draws nothing of its own — no panel, no min-height, no margin — and the space
   appears only with the box (or the wait line). */
.w-turnstile--live { display: block; background: none; box-shadow: none; padding: 0; min-height: 0; margin-bottom: 0; border-radius: 0; }
.w-turnstile--live .cf-turnstile iframe { margin-bottom: var(--cn-v2-space-lg); }
.w-ts-wait { margin: 0 0 var(--cn-v2-space-lg); font-size: var(--cn-v2-type-size-body-app); color: var(--cn-v2-content-secondary); }

/* ── Progress — the system's panel pattern, widget-owned ──────────────────────── */
.w-progress {
  display: flex; flex-direction: column; gap: var(--cn-v2-space-sm);
  max-width: 360px; width: 100%; box-sizing: border-box;
  background: var(--cn-v2-surface-default);
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-subtle);
  border-radius: var(--cn-v2-radius-panel); padding: var(--cn-v2-space-lg);
  margin: var(--cn-v2-space-md) 0;
}
/* The caption sits ABOVE the bar, as the system's panel does — a number underneath an
   unlabelled bar reads as a caption for whatever follows it. */
.w-progress-caption {
  display: flex; justify-content: space-between; gap: var(--cn-v2-space-sm);
  margin: 0; color: var(--cn-v2-content-secondary);
  font-size: var(--cn-v2-type-size-caption); line-height: var(--cn-v2-type-line-caption);
}
.w-progress-track {
  background: var(--cn-v2-surface-raised); border-radius: var(--cn-v2-radius-full);
  height: 6px; width: 100%; overflow: hidden; position: relative; box-sizing: border-box;
}
.w-progress-fill {
  display: block; height: 100%; width: 0%; border-radius: inherit;
  background: var(--cn-v2-action-primary-background);
  transition: width var(--cn-v2-motion-duration-slow) var(--cn-v2-motion-easing-standard);
}
/* NO INDETERMINATE ANIMATION (CTO 2026-10-09). The old one-shot 8%→92% keyframe restarted
   on every poll re-render — the climb-then-reset bug. The fill's width is the real,
   monotonic value (widget.js _progressValue) and moves only by the transition above. */

/* ── Divider ─────────────────────────────────────────────────────────────────── */
.w-divider {
  display: flex; align-items: center; gap: var(--cn-v2-space-md);
  margin: var(--cn-v2-space-xl) 0; color: var(--cn-v2-content-muted);
  font-size: var(--cn-v2-type-size-caption); line-height: var(--cn-v2-type-line-caption);
}
.w-divider::before, .w-divider::after {
  content: ""; height: var(--cn-v2-stroke-hairline); background: var(--cn-v2-border-subtle); flex: 1;
}

/* ── Result card (CTO 2026-10-07, mockup-card.html) ────────────────────────────── */
/* MONO IS FOR NUMBERS ONLY: a figure is wrapped in .w-num and takes the mono family;
   every word around it — "from", "/ month", "installation (one-off)", the labels, the
   service and medium names — is Inter. Colour: the eyebrow is status-healthy because it
   is a live status shown WITH a word; the tick on the "Price unlocked" line is the only
   other green. The price is content-strong, never green. */
.w-num { font-family: var(--w-mono); }
/* The eyebrow: the system's label style (11px, uppercase, 0.18em tracking) with a dot.
   It is an <h3> for the heading semantics; the style is the label's, not the heading's. */
.w-eyebrow {
  display: flex; align-items: center; gap: var(--cn-v2-space-sm);
  margin: 0 0 var(--cn-v2-space-md); font-family: var(--w-sans);
  font-size: var(--cn-v2-type-size-label); line-height: var(--cn-v2-type-line-label);
  font-weight: var(--cn-v2-type-weight-semibold); letter-spacing: var(--cn-v2-type-tracking-label);
  text-transform: uppercase; color: var(--cn-v2-status-healthy);
}
.w-eyebrow__dot {
  flex: 0 0 auto; width: 6px; height: 6px; border-radius: var(--cn-v2-radius-full);
  background: currentColor;
}
/* THE CARD'S RHYTHM (mockup-card.html): the head is a tight group — eyebrow, then the
   address as the card's title line at the subheading size in content-strong, then the
   carriers line a hair beneath it — and the hero block stands off from it by space-xl.
   The address stays Inter and regular (divergence 1: the prospect's own words; the
   mockup's medium is not a shipped weight, and semibold would shout over the price). */
.w-addr--card {
  font-size: var(--cn-v2-type-size-subheading); line-height: var(--cn-v2-type-line-subheading);
  letter-spacing: var(--cn-v2-type-tracking-subheading);
  color: var(--cn-v2-content-strong); margin: 0 0 var(--cn-v2-space-xs);
}
/* "N carriers can serve this address" — muted body text; the count is a number. AA on
   both surfaces: content-muted #a0a0a0 is 6.6:1 on surface-default and 5.5:1 on
   surface-raised (tests/test_widget_frontend.py computes it from the token file). */
.w-carriers {
  font-size: var(--cn-v2-type-size-body-app); line-height: var(--cn-v2-type-line-body-app);
  color: var(--cn-v2-content-muted); margin: 0;
}
.w-spec { margin: var(--cn-v2-space-xl) 0 0; }
/* Several bandwidths render as repeated .w-tier blocks separated by a stronger rule. */
.w-tier + .w-tier {
  margin-top: var(--cn-v2-space-xl); padding-top: var(--cn-v2-space-xl);
  border-top: var(--cn-v2-stroke-focus) solid var(--cn-v2-border-default);
}
/* The hero line: "from £520 / month". The FIGURE is .w-price — Mono 600 at the display
   size (Mono ships 400/600 only, so 700 is not available and is NOT synthesised). The
   words either side are Inter at the subheading size, content-secondary. */
.w-hero {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 var(--cn-v2-space-sm);
  margin: 0;
}
.w-hero__from, .w-hero__per {
  font-family: var(--w-sans); font-size: var(--cn-v2-type-size-subheading);
  line-height: var(--cn-v2-type-line-subheading); font-weight: var(--cn-v2-type-weight-regular);
  letter-spacing: normal; color: var(--cn-v2-content-secondary);
}
.w-price {
  font-family: var(--w-mono); font-size: var(--cn-v2-type-size-display);
  line-height: var(--cn-v2-type-line-display); font-weight: var(--cn-v2-type-weight-semibold);
  letter-spacing: var(--cn-v2-type-tracking-display); color: var(--cn-v2-content-strong);
}
/* LOCKED: the price is ONE field withheld — the blurred placeholder sits in the hero
   line like the figure it stands in for. No real digits are ever in the DOM while
   locked — the server withholds them (R2) and fixture mode renders the placeholder,
   never the number. */
/* LOCKED: "£•••" — a placeholder, not digits; no real number is in the page (R2). */
.w-price--locked {
  color: var(--cn-v2-content-secondary); letter-spacing: 0.04em;
  user-select: none; -webkit-user-select: none;
}
/* The spec line under the address while searching: "1 Gbps · Dedicated internet, 36 months". */
.w-specline {
  margin: 0 0 var(--cn-v2-space-lg); font-size: var(--cn-v2-type-size-body-app);
  line-height: var(--cn-v2-type-line-body-app); color: var(--cn-v2-content-primary);
}
.w-searching-copy { margin-bottom: var(--cn-v2-space-lg); }
.w-unlock-hint { margin: var(--cn-v2-space-sm) 0 0; }
/* The contact panel beneath the card: a raised panel, one field per row at the base
   (the company/phone pair goes two-up only in the wide layout), a full-width button. */
.w-capture { min-width: 0; }
.w-panel {
  margin-top: var(--cn-v2-space-xl); padding: var(--cn-v2-space-lg);
  background: var(--cn-v2-surface-raised); border-radius: var(--cn-v2-radius-panel);
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-subtle);
}
.w-panel .w-h3 { margin-top: 0; }
.w-panel .w-btn { width: 100%; }
.w-received { margin-top: var(--cn-v2-space-lg); }
/* The polite live region the panel's appearance is announced through: visually hidden,
   present from init (outside the re-rendered body), never display:none. */
.w-sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* "£588 installation (one-off)" — body text, the figure in mono and one step brighter
   than the words (content-primary, as the mockup has it). */
.w-install {
  margin: var(--cn-v2-space-sm) 0 0;
  font-size: var(--cn-v2-type-size-body-app); line-height: var(--cn-v2-type-line-body-app);
  color: var(--cn-v2-content-secondary);
}
.w-install .w-num { color: var(--cn-v2-content-primary); }
/* The one rule between the figures and the spec list, and the one before the unlocked
   line. A real <hr>, so forced colours draw it. More air above it than below: the rule
   closes the block before it (mockup: 26/20 and 20/16 → space-xl over space-lg). */
.w-rule {
  border: 0; border-top: var(--cn-v2-stroke-hairline) solid var(--cn-v2-border-subtle);
  margin: var(--cn-v2-space-xl) 0 var(--cn-v2-space-lg); height: 0;
}
/* The spec list: Bandwidth / Service / Delivered over, with NO row dividers. A two-column
   grid as in the mockup — a fixed 140px label column with the value reading on directly
   beside it, NOT a label/value pair pushed to opposite edges, which at 440-560px left
   nothing between them. Label and value share the body size; the label is muted and
   the value content-primary, so colour alone carries the pairing. Numbers mono. */
.w-spec-list { margin: 0; padding: 0; }
.w-spec-row {
  display: grid; grid-template-columns: 140px minmax(0, 1fr);
  column-gap: var(--cn-v2-space-lg); align-items: baseline;
  margin: 0; padding: 0; border: 0;
}
.w-spec-row + .w-spec-row { margin-top: var(--cn-v2-space-md); }
.w-spec-row__label {
  margin: 0; font-family: var(--w-sans); font-size: var(--cn-v2-type-size-body-app);
  line-height: var(--cn-v2-type-line-body-app); font-weight: var(--cn-v2-type-weight-regular);
  letter-spacing: normal; text-transform: none; color: var(--cn-v2-content-muted);
}
.w-spec-row__value {
  margin: 0; font-family: var(--w-sans); font-size: var(--cn-v2-type-size-body-app);
  line-height: var(--cn-v2-type-line-body-app); color: var(--cn-v2-content-primary);
  text-align: left; overflow-wrap: anywhere;
}
/* The footnotes: the first in content-secondary, the rest muted (AA on both surfaces). */
.w-footnotes { list-style: none; margin: var(--cn-v2-space-xl) 0 0; padding: 0; }
.w-footnote {
  font-size: var(--cn-v2-type-size-caption); line-height: var(--cn-v2-type-line-caption);
  color: var(--cn-v2-content-muted); margin: 0;
}
.w-footnote + .w-footnote { margin-top: var(--cn-v2-space-sm); }
.w-footnote--primary { color: var(--cn-v2-content-secondary); }
/* The transient "↓ improved" marker. It carries a GLYPH, so it needs no colour: it is
   content-coloured, because green belongs to the eyebrow and the tick alone. */
.w-improved {
  font-family: var(--w-sans); font-size: var(--cn-v2-type-size-caption);
  line-height: var(--cn-v2-type-line-caption); font-weight: var(--cn-v2-type-weight-regular);
  letter-spacing: normal; text-transform: none;
  color: var(--cn-v2-content-secondary); margin-left: var(--cn-v2-space-sm); white-space: nowrap;
  opacity: 0;
  animation: w-improved-in var(--cn-v2-motion-duration-standard)
             var(--cn-v2-motion-easing-standard) forwards;
}
@keyframes w-improved-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }
/* Per-component static fallback: with motion off the marker must still be VISIBLE. The
   blanket `* { animation-duration: .01ms !important }` this replaces would have
   overridden exactly this kind of rule (and froze #241's shimmer mid-slide). */
@media (prefers-reduced-motion: reduce) {
  .w-improved { opacity: 1; transform: none; animation: none; }
  .w-progress-fill { transition: none; }
}
.w-still-checking { margin: var(--cn-v2-space-md) 0; }

/* The unlock prompt (locked reveal) — selection tint, hairline, lock glyph. */
.w-unlock {
  display: flex; align-items: center; gap: var(--cn-v2-space-sm); flex-wrap: wrap;
  margin: var(--cn-v2-space-lg) 0 var(--cn-v2-space-md);
  font-size: var(--cn-v2-type-size-body-app); line-height: var(--cn-v2-type-line-body-app);
  font-weight: var(--cn-v2-type-weight-semibold); color: var(--cn-v2-content-strong);
  padding: var(--cn-v2-space-md) var(--cn-v2-space-lg);
  background: var(--cn-v2-selection-background);
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-active);
  border-radius: var(--cn-v2-radius-panel);
}
.w-unlock::before { content: "\1F512"; font-size: var(--cn-v2-type-size-body-app); }

/* Success — a flush paragraph, NOT a flex row: the ✓ is an INLINE ::before so the line
   wraps as ONE paragraph at narrow widths (a flex row hangs the wrapped lines under the
   tick and orphans a long email). overflow-wrap breaks an over-long address. */
.w-success {
  color: var(--cn-v2-content-secondary); font-size: var(--cn-v2-type-size-body-app);
  line-height: var(--cn-v2-type-line-body-app); margin-top: var(--cn-v2-space-md);
  overflow-wrap: break-word;
}
.w-success::before {
  content: "\2713"; margin-right: var(--cn-v2-space-sm);
  color: var(--cn-v2-status-healthy); font-weight: var(--cn-v2-type-weight-semibold);
}

/* ── Lead-payload preview (fixture mode only) ────────────────────────────────── */
.w-payload {
  margin-top: var(--cn-v2-space-lg); background: var(--cn-v2-surface-canvas);
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-hairline) var(--cn-v2-border-active);
  border-radius: var(--cn-v2-radius-panel); padding: var(--cn-v2-space-lg);
}
.w-payload h4 {
  margin: 0 0 var(--cn-v2-space-sm); font-family: var(--w-sans);
  font-size: var(--cn-v2-type-size-caption); line-height: var(--cn-v2-type-line-caption);
  font-weight: var(--cn-v2-type-weight-semibold); color: var(--cn-v2-content-strong);
}
.w-payload pre {
  margin: 0; font-family: var(--w-mono); font-size: var(--cn-v2-type-size-mono);
  line-height: var(--cn-v2-type-line-mono); color: var(--cn-v2-content-secondary);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.w-payload .w-muted { margin-top: var(--cn-v2-space-sm); }

/* Errors carry a "!" glyph as well as the critical colour — never colour alone. */
.w-error-inline {
  color: var(--cn-v2-status-critical); font-family: var(--w-sans);
  font-size: var(--cn-v2-type-size-caption); line-height: var(--cn-v2-type-line-caption);
  margin: var(--cn-v2-space-xs) 0; display: flex; gap: var(--cn-v2-space-xs);
}
.w-error-inline::before { content: "!"; font-weight: var(--cn-v2-type-weight-semibold); flex: 0 0 auto; }
.w-pending { padding: var(--cn-v2-space-sm) 0; }

/* ── Focus ────────────────────────────────────────────────────────────────────
   An INSET 2px ring plus the halo — the system's pattern, and the one v3's "no glow
   anywhere" rule banned (that test is retired in this commit: the halo is part of the
   affordance, not decoration).

   REASONED DIVERGENCE 3 — THE PRIMARY CTA TAKES A DIFFERENT RING COLOUR, and here the
   SYSTEM IS WRONG, not us. Its documented Button focus treatment is an inset 2px ring of
   --cn-v2-focus-ring, which is invisible by construction on a primary button, because
   both tokens are the same colour:

       focus-ring #00ffa3 on action-primary-background #00ffa3  =  1.00:1
       the halo, rgba(0,255,163,.35) composited on surface #1c1c1c
         -> rgb(18,107,75) on #1c1c1c                            =  2.62:1  (floor is 3:1)

   So the widget's primary control had NO visible keyboard focus indicator at all, in
   either contrast mode — WCAG 2.2 2.4.7 and 1.4.11, not a nicety. The CTA therefore
   rings in --cn-v2-action-primary-content (#000), still a semantic token, no primitive
   and no invented colour:

       action-primary-content #000 on action-primary-background #00ffa3 = 15.83:1

   Everything else keeps --cn-v2-focus-ring: ghost buttons, fields, predictions and the
   checkbox sit on dark surfaces, where it is correct and high-contrast.

   AND THE HALO IS COMPOSED FROM THE COLOUR TOKEN, not consumed as --cn-v2-effect-focus-
   halo. That composed token is ABSENT from the [data-cn-contrast="high"] block, while
   --cn-v2-focus-halo IS re-pointed there (rgba(0,255,163,.35) -> #00ffa3, 14.65:1 on the
   high-contrast surface). Consuming the composed effect silently opted the widget out of
   high contrast entirely. Restating the 3px geometry is deliberate: the geometry is
   structural and has no token of its own, whereas the COLOUR is the thing that must
   track contrast mode. */
.widget :focus-visible, .demo-bar :focus-visible {
  outline: 0;
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-focus) var(--cn-v2-focus-ring),
              0 0 0 3px var(--cn-v2-focus-halo);
}
/* :not(.w-btn--ghost) — the ghost button carries both classes and must keep focus-ring,
   because it sits on the panel surface, not on green. */
.w-btn:not(.w-btn--ghost):focus-visible {
  outline: 0;
  box-shadow: inset 0 0 0 var(--cn-v2-stroke-focus) var(--cn-v2-action-primary-content),
              0 0 0 3px var(--cn-v2-focus-halo);
}
/* The checkbox draws its ring on the MARK (the input itself is 1px and invisible), so
   it must not also take the generic inset ring. */
.w-check input[type="checkbox"]:focus-visible { box-shadow: none; }

/* ── High contrast ───────────────────────────────────────────────────────────── */
/* The widget renders in an IFRAME and cannot inherit the host page's
   data-cn-contrast attribute, so the token file's [data-cn-contrast="high"] block is
   reachable two ways: the attribute is set on the widget's own <html> (from
   __WIDGET_CONFIG__.contrast, by the bootstrap), and prefers-contrast: more sets it too.
   Both routes end at the SAME vendored token block — no values are restated here, which
   is why there is nothing to drift. */

/* ── Forced colours (Windows High Contrast) ──────────────────────────────────── */
/* The progress fill is a BACKGROUND colour, so it vanished entirely here: an empty
   track reading as "no progress". Every state cue that is a background gets
   forced-color-adjust: none and a system colour; everything else is left to the OS. */
@media (forced-colors: active) {
  html, body { background: Canvas; color: CanvasText; }
  .widget, .widget * { color: CanvasText; }
  .w-input::placeholder { color: GrayText; opacity: 1; }
  .w-input, .w-select, .w-static, .w-btn, .w-btn--ghost, .w-prediction {
    border: var(--cn-v2-stroke-hairline) solid ButtonText; forced-color-adjust: auto;
  }
  .w-btn, .w-btn * { background: Highlight; color: HighlightText; forced-color-adjust: none; }
  .w-btn:disabled { color: GrayText; }
  .widget :focus-visible, .demo-bar :focus-visible,
  .w-btn:not(.w-btn--ghost):focus-visible {
    outline: 3px solid Highlight; outline-offset: 2px; box-shadow: none;
  }
  /* Documented state cues that are otherwise invisible: the track outline, the fill,
     the checked mark and the healthy dot. */
  .w-progress-track, .w-check input[type="checkbox"] + span::before, .w-turnstile:not(.w-turnstile--live) {
    border: var(--cn-v2-stroke-hairline) solid CanvasText; box-sizing: border-box;
  }
  .w-progress-fill { background: Highlight; forced-color-adjust: none; }
  .w-check input[type="checkbox"]:checked + span::before {
    background: Highlight; color: HighlightText; forced-color-adjust: none;
  }
  .w-improved, .w-success::before, .w-error-inline, .w-eyebrow { color: CanvasText; }
  .w-eyebrow__dot { background: CanvasText; forced-color-adjust: none; }
  .w-rule { border-top-color: CanvasText; }
  .w-map__img { border: var(--cn-v2-stroke-hairline) solid CanvasText; box-shadow: none; }
}

/* ── Responsive ──────────────────────────────────────────────────────────────── */
/* Breakpoints are the system's: mobile 480, tablet 768, desktop 1200, wide 1440. The
   widget is a single column at every one of them — what changes is the gutter and the
   internal padding, so it lays out cleanly from 288px of container upwards. */
@media (max-width: 480px) {
  body { padding: var(--cn-v2-space-md); gap: var(--cn-v2-space-md); }
  .widget__body { padding: var(--cn-v2-space-md); }
  .w-progress { padding: var(--cn-v2-space-md); }
}
/* The spec row stacks ONLY where the pair genuinely cannot share a line. "Dedicated
   internet, 36 months" fits beside its label at 480 but not at 375 — so the stack is
   pinned to the width where it is forced, not to the mobile breakpoint. */
@media (max-width: 420px) {
  .w-spec-row { grid-template-columns: minmax(0, 1fr); row-gap: var(--cn-v2-space-2xs); }
}
@media (min-width: 768px) {
  body { padding: var(--cn-v2-layout-gutter-tablet); }
}
