/* ── DERIVED FROM THE REGION MAPS — DO NOT HAND-EDIT ──────────────────────
   site/_build/derive-face-floor.js rewrites this block on every build from
   site/assets/baked/regions.json. Widest look: framed-v1-pro with 10 regions.
   Floor = 264px — JON-RULED anti-collapse floor, NOT regions x tap.
   The widest look has 10 regions; that is reported, not multiplied.
   The regions x 44 premise was retired by the tappability ruling.
   Change the maps, not these numbers. */
:root {
  --pf-tap-floor: 44px;
  --pf-face-regions: 10;
  --pf-face-ratio-w: 210;
  --pf-face-ratio-h: 297;
  --pf-face-floor-h: 264px;
  /* derived from the ratio TOKEN, not restated. Two places both saying
     210/297 is two places that have to be remembered together, and the
     landscape measurement found exactly that: the face held portrait
     proportions with the orientation control set to landscape. */
  --pf-face-floor-w: calc(var(--pf-face-floor-h) * var(--pf-face-ratio-w) / var(--pf-face-ratio-h));
  --pf-thumb-min-w: 44px;
  --pf-thumb-min-h: 73px;
  --pf-thumb-h: clamp(var(--pf-thumb-min-h), 8svh, 88px);
  --pf-face-chrome: calc(var(--pf-thumb-h) + 88px + 44px);
  /* thinnest band across every look: back-menu/logo at 0.0175 of
     the face. The zoom ceiling is 44px / (this x the live face height), worked
     out at paint because the face height is not knowable here. */
  --pf-thinnest-band: 0.0175;
}

/* ── HOW BIG A CELL MAY GET — NOT DERIVED FROM THE REGION MAPS, SO IT LIVES
   OUTSIDE THE BLOCK THAT SAYS DO NOT HAND-EDIT ──────────────────────────────
   The first version of this token was written INSIDE the derived block and
   `derive-face-floor.js` wiped it on the very next build — the file says so in
   its first line, and the walk found a cell still 808px tall with the cap
   sitting in the source. The maps do not know the cell (they hold the 18 solo
   and back looks), so this cannot be derived there.

   Jon-ruled 2026-08-24: "report the size at which a cell's bands are
   comfortably editable and cap there". MEASURED per look as the band's share
   of the cell it sits on — the only scale-free way to ask it
   (probe-cell-band-floor.js), identical at 1600 and 390:
     offer-led    contact 11.42%  ->  386px for a 44px band
     split        offer    9.87%  ->  446px
     voucher-led  contact  7.37%  ->  598px
     hero-led     offer    6.61%  ->  666px
     cell         terms    2.87%  -> 1539px   <- the small print, see below
   446px is the LARGEST of these that still leaves the Shared design section
   inside one screenful at 1600: the section is the cell plus 357px of measured
   furniture, so 803 of 808. It buys 44px bands on offer-led and split; the two
   quad looks keep 33px and 36px bands, and ZOOM is the ratified remedy for a
   band under 44px (gate-zoom-levels holds it). The 1539px line is the trap
   that retired the solo floor derivation on 2026-08-21 — a terms line at 2.87%
   would demand a cell taller than any screen to satisfy a rule made to serve
   it. Jon rules this number; it is one literal with its derivation above it. */
:root { --pf-cell-cap: 446px; }

/* ── ONE FACE AT A TIME, AT EVERY WIDTH (Jon-ruled 2026-08-24) ────────────
   SUPERSEDES the side-by-side desktop gallery, and the `--pf-face-pair-min`
   token that had just been derived for it — both are deleted rather than left
   to rot. Measured before the ruling, at 1600x900: two faces give 322x451
   each; ONE face gives **396x555** — +23% linear and 1.5x the area — because
   past about 392px of face width the picture stops being sized by its column
   and starts being sized by the section, which is one screenful less the
   furniture (808 - 253 = 555).
   The second face is a flip away, exactly as the handset has always done it,
   and "Full size" is where both can stand together — reported in
   DECISIONS-PENDING 2026-08-24 (p) with the room measured. */

/* ==========================================================================
   PromoFlyers — site design system
   Tokens from docs/BRAND.md. No ad-hoc values: colours, spacing (8px scale),
   radii, shadows and type all come from the custom properties below.
   Orange is for CTAs, active states and key highlights ONLY.
   ========================================================================== */

:root {
  /* Brand colour tokens (docs/BRAND.md). ONE source: --pf-brand (var(--pf-brand),
     ratified). Every brand-orange alias points at it; deliberate shades are
     derivations of the same value (documented, not free-standing colours). */
  --pf-brand: #FD3601;
  --pf-orange: var(--pf-brand);
  --orange: var(--pf-brand);      /* legacy alias used by var(--orange, ...) fallbacks */
  --pf-orange-dark: #E22F00;      /* = brand darkened ~11% for hover (FD3601 -> E22F00) */
  /* TEXT-BEARING ORANGE (Jon ruled the 3.68:1 shortfall out, 2026-08-11).
     White on brand orange is 3.68:1 — under the 4.5:1 floor. This is the
     same orange taken down until white clears it (5.04:1), and it is used
     ONLY where orange is a FILL WITH TEXT ON IT. Brand orange keeps
     everything else — the wordmark, borders, tints, non-text fills — so
     this is two tokens doing two jobs, not a brand change.
     (--pf-orange-dark, the hover step, measures 4.53:1 — it scrapes the
     floor by three hundredths, which is no margin at all for a value
     someone may nudge later, and it carries a hover meaning. Kept
     separate deliberately.) */
  --pf-orange-deep: #D62800;
  --pf-orange-tint: rgba(253, 54, 1, 0.06); /* = brand at 6% alpha */
  --pf-black: #1A1A1A;
  --pf-grey-600: #6B6B6B;
  --pf-grey-400: #9A9A9A;         /* decorative/large text only — fails AA small */
  --pf-grey-50: #F9F9F9;
  --pf-border: #E8E8E8;
  --pf-seg-border: #C9C0B4;       /* slot segments: empty cells must read on the warm stage (MOBILE-POLISH-06) */
  --pf-white: #FFFFFF;
  /* ── THE ONE GREEN LAW (Jon-ruled 2026-08-09) ──────────────────────
     ONE green token for the whole site: the FLYER'S OWN FOREST REGISTER
     (#143528 — the same value the flagship's strip prints), used by the
     completion chrome, the per-letterbox figure, savings, trust ticks and
     every other green surface. No second green anywhere.
     VALUE SETTLED IN THREE STEPS, each one Jon's eye on the real screen:
       #143528  read as BLACK — 13.38:1 is a lot of contrast and no hue
       the second read as TOO GREEN — it competed with the orange
       #175F3F  the resting forest: 7.65:1 on white, 7.17:1 on cream —
                unmistakably green, deep enough to sit UNDER the orange
                rather than beside it. Completion should read as calm.
     The two states are 2.08:1 apart in luminance, so they are told apart
     by HUE — which some customers cannot use. The GLYPH carries the
     meaning either way: a tick when done, a number when it is your turn.
     THIS TOKEN ALSO CLOSED A CONTRAST DEFECT: the old #00B67A measured
     2.63:1 on white while carrying TEXT (the per-letterbox line, the
     savings value) — a plain AA failure. */
  --pf-green: #175F3F;
  /* (--pf-green-on-dark RETIRED 2026-08-10 with the colour mirroring it
     existed for: the bar ticks now, it does not colour.) */
  --pf-gold: #FFD700;

  /* Spacing — strict 8px scale */
  --s-1: 8px;
  --stack-gap: 12px; /* MOBILE-TIDY-04: one gap between phone landing blocks */
  --s-2: 16px;
  --s-3: 24px;
  --s-4: 32px;
  --s-6: 48px;
  --s-8: 64px;
  --s-12: 96px;
  --s-16: 128px;

  /* Radii + shadow */
  --r-btn: 10px; /* THE button token (Jon's one-corner law, 3rd raising 2026-08-02): every button + box shares it; the in-content family (shapes/terms/tiles/segs) is the reference */
  --r-card: 16px;
  --r-pill: 999px;
  --shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.06);

  /* Layout */
  --container: 1140px;
  --gutter: var(--s-3);            /* 24px mobile */

  /* Type scale (BRAND.md / WEEK-1), fluid to mobile */
  --fs-h1: clamp(36px, 3.2vw + 24px, 56px);
  --lh-h1: 1.15;
  --ls-h1: -1.5px;
  --fs-h2: clamp(28px, 2vw + 20px, 40px);
  --lh-h2: 1.2;
  --ls-h2: -1px;
  --fs-h3: clamp(22px, 1vw + 18px, 28px);
  --lh-h3: 1.22;
  --fs-body: 17px;
  --lh-body: 28px;
  --fs-small: 14px;
  --lh-small: 22px;

  /* Section rhythm: 64 mobile → 112 desktop (within the 96–128 band) */
  --section-y: var(--s-8);
}

@media (min-width: 768px) {
  :root { --gutter: var(--s-6); --section-y: var(--s-12); }
}
@media (min-width: 1140px) {
  :root { --section-y: 112px; }
}

/* --------------------------------------------------------------------------
   Base
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { scrollbar-gutter: stable; } /* tab switches must not shift edges via scrollbar */
@media (max-width: 768px) { html { scroll-padding-top: 150px; } } /* content never lands under the sticky preview strip */
* { margin: 0; padding: 0; }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

body {
  font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--pf-black);
  background: var(--pf-white);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img { max-width: 100%; height: auto; display: block; }

h1, h2, h3, h4 { font-weight: 800; color: var(--pf-black); text-wrap: balance; }
h1 { font-size: var(--fs-h1); line-height: var(--lh-h1); letter-spacing: var(--ls-h1); }
h2 { font-size: var(--fs-h2); line-height: var(--lh-h2); letter-spacing: var(--ls-h2); font-weight: 700; }
h3 { font-size: var(--fs-h3); line-height: var(--lh-h3); font-weight: 700; letter-spacing: -0.3px; }
h4 { font-size: var(--fs-body); font-weight: 700; }

p, li { max-width: 65ch; }
.muted { color: var(--pf-grey-600); }
.small { font-size: var(--fs-small); line-height: var(--lh-small); }

a { color: inherit; }
a:not([class]) { color: var(--pf-black); text-underline-offset: 3px; }
a:not([class]):hover { color: var(--pf-orange); }

:focus-visible {
  outline: 2px solid var(--pf-orange);
  outline-offset: 2px;
  border-radius: 4px;
}

.container {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: var(--section-y); }
.section--grey { background: var(--band, #FAF7F4); }
.section-head { margin-bottom: var(--s-6); }
.section-head p { margin-top: var(--s-2); max-width: 70ch; }

.eyebrow {
  display: inline-block;
  font-size: var(--fs-small);
  line-height: var(--lh-small);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--pf-orange);
  margin-bottom: var(--s-2);
}

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* Skip link */
.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--pf-black); color: var(--pf-white);
  padding: var(--s-1) var(--s-2); border-radius: 0 0 var(--r-btn) 0;
}
.skip-link:focus { left: 0; }

/* --------------------------------------------------------------------------
   Buttons — hover, focus-visible and active states everywhere
   -------------------------------------------------------------------------- */
.btn {
  display: inline-block;
  /* 19px/700 keeps white-on-brand-orange at the WCAG large-text threshold (3:1) */
  font: 700 19px/1.2 Inter, system-ui, sans-serif;
  padding: 14px var(--s-3);
  border-radius: var(--r-btn);
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
  text-align: center;
  transition: background-color 0.15s ease, transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.btn-primary { background: var(--pf-orange); color: var(--pf-white); }
.btn-primary:hover { background: var(--pf-orange-dark); transform: translateY(-1px); box-shadow: var(--shadow-hover); }
.btn-primary:active { transform: translateY(0); box-shadow: none; }
.btn-secondary { background: var(--pf-white); color: var(--pf-black); border-color: var(--pf-border); }
.btn-secondary:hover { border-color: var(--pf-black); transform: translateY(-1px); }
.btn-secondary:active { transform: translateY(0); }
.btn-small { padding: 11px var(--s-3); font-size: 19px; }
/* Focus ring must be visible ON the orange button itself */
.btn-primary:focus-visible { outline-color: var(--pf-black); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: 0.55; cursor: default; transform: none; }

.text-link {
  font-weight: 600; color: var(--pf-black);
  text-decoration: underline; text-underline-offset: 3px;
  display: inline-block; padding-block: 10px; /* 44px touch target */
  background: none; border: 0; cursor: pointer; font-size: inherit;
}
.text-link:hover { color: var(--pf-orange); }

/* --------------------------------------------------------------------------
   Header
   -------------------------------------------------------------------------- */
/* ── THE STICKY STACK (Jon's defect, 2026-08-10) ───────────────────────
   The guide bar and the header both stuck at top:0, so on any scroll the
   bar (z-400) sat ON the header (z-50) and swallowed the logo. They are
   one stack now: the bar owns the top, the header sticks DIRECTLY BENEATH
   it by the bar's own measured height (--pf-bar-h, written by the bar's
   only writer whenever it renders, shows, hides or resizes). Nothing
   guesses a height, so nothing drifts when the bar's copy wraps. */
.site-header {
  position: sticky; top: var(--pf-bar-h, 0px); z-index: 50;
  background: var(--pf-white);
  border-bottom: 1px solid var(--pf-border);
}
.nav-row {
  display: flex; align-items: center;
  gap: var(--s-3); min-height: 72px; flex-wrap: wrap;
  padding-block: var(--s-1);
}
.brand { display: flex; align-items: center; padding-block: var(--s-1); min-height: 44px; order: 1; }
.brand img { width: 160px; height: auto; }
.nav-cta { order: 3; margin-left: auto; white-space: nowrap; }
.nav-cta .nav-cta-short { display: none; }
@media (max-width: 767px) {
  .nav-cta .nav-cta-full { display: none; }
  .nav-cta .nav-cta-short { display: inline; }
}
.site-nav { display: flex; align-items: center; gap: var(--s-3); order: 2; margin-left: auto; }
.site-nav ul {
  display: flex; gap: var(--s-3); list-style: none;
  overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.site-nav ul::-webkit-scrollbar { display: none; }
/* Scoped to the link LIST so the .btn CTA keeps its own button styling */
.site-nav ul a {
  font-size: 15px; font-weight: 600; text-decoration: none;
  color: var(--pf-black); white-space: nowrap;
  padding: 12px 0; border-bottom: 2px solid transparent; /* ≥44px touch target */
}
.site-nav ul a:hover { color: var(--pf-orange); }
.site-nav ul a[aria-current="page"] { color: var(--pf-orange); border-bottom-color: var(--pf-orange); }
@media (max-width: 767px) {
  /* Row 1: logo left + CTA right · Row 2: full-width scrollable links */
  .nav-row { gap: var(--s-2); }
  .nav-cta { order: 2; }
  .site-nav { order: 3; flex: 1 1 100%; margin-left: 0; }
  .site-nav ul { width: 100%; }
}
@media (max-width: 359px) {
  /* Tiny screens: logo and CTA can't share 272px — full-width CTA reads intentional */
  .nav-cta { flex: 1 1 100%; text-align: center; }
}

/* --------------------------------------------------------------------------
   Hero
   -------------------------------------------------------------------------- */
.hero { padding-block: var(--section-y); }
.hero-grid {
  display: grid; gap: var(--s-6); align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 900px) { .hero-grid { grid-template-columns: 1.05fr 1fr; } }
.hero p.lead { margin-top: var(--s-3); font-size: clamp(17px, 1vw + 14px, 20px); line-height: 1.6; color: var(--pf-grey-600); }
.hero-ctas { display: flex; flex-wrap: wrap; gap: 12px; margin-top: var(--s-4); }
.hero-note { margin-top: var(--s-2); }

/* --------------------------------------------------------------------------
   Card grid mockup — the 8-slot PromoFlyers Card, reusable
   Slots fill live from /api/placeholders; static styled fallback beneath.
   -------------------------------------------------------------------------- */
.card-mock {
  background: var(--pf-white);
  border: 1px solid var(--pf-border);
  border-radius: var(--r-card);
  padding: var(--s-2);
  box-shadow: var(--shadow-hover);
}
.card-mock-brand {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--s-1) var(--s-1) var(--s-2);
}
.card-mock-brand .wordmark { font-weight: 800; font-size: 19px; letter-spacing: -0.3px; }
.card-mock-brand .wordmark b { color: var(--pf-orange); }
.card-mock-brand .side { font-size: 12px; line-height: var(--lh-small); font-weight: 600; color: var(--pf-grey-600); }
.card-mock-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-1);
}
.mock-slot {
  position: relative; aspect-ratio: var(--pf-cell-ratio-w) / var(--pf-cell-ratio-h); border-radius: var(--s-1);
  overflow: hidden; border: 1px solid var(--pf-border); background: var(--pf-grey-50);
}
.mock-slot img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.mock-slot .fallback {
  position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: flex-end;
  padding: var(--s-1);
}
.mock-slot .fallback .bar { height: 6px; width: 60%; border-radius: var(--r-pill); background: var(--pf-border); margin-bottom: 6px; }
.mock-slot .fallback .bar.accent { width: 40%; background: var(--pf-orange); opacity: 0.85; }
.mock-slot.your-slot { border: 2px solid var(--pf-orange); background: var(--pf-orange-tint); }
.mock-slot.your-slot .label {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 19px; color: var(--pf-orange); text-align: center;
}
.card-mock-caption { margin-top: var(--s-2); text-align: center; }

/* --------------------------------------------------------------------------
   Product grid
   -------------------------------------------------------------------------- */
.product-grid {
  display: grid; gap: var(--s-3);
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.product-card {
  display: flex; flex-direction: column; gap: var(--s-2);
  background: var(--pf-white);
  border: 1px solid var(--pf-border);
  border-radius: var(--r-card);
  padding: var(--s-4);
  text-decoration: none;
  transition: box-shadow 0.15s ease, transform 0.15s ease, border-color 0.15s ease;
}
a.product-card:hover { box-shadow: var(--shadow-hover); transform: translateY(-2px); border-color: var(--pf-grey-400); }
a.product-card:active { transform: translateY(0); }
.product-card h3 { font-size: 19px; line-height: 1.3; }
.product-card p { font-size: 15px; line-height: 1.55; color: var(--pf-grey-600); flex: 1; }
.product-card .meta { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
.price { font-weight: 700; color: var(--pf-black); }
.price .from { font-weight: 500; color: var(--pf-grey-600); font-size: var(--fs-small); }
.arrow { color: var(--pf-orange); font-weight: 700; }
.badge {
  display: inline-block; font-size: 12px; line-height: 1; font-weight: 700;
  letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--pf-grey-600); background: var(--pf-grey-50);
  border: 1px solid var(--pf-border); border-radius: var(--r-pill);
  padding: 6px 10px;
}
.badge--founding { color: var(--pf-orange); background: var(--pf-orange-tint); border-color: transparent; }

/* --------------------------------------------------------------------------
   Steps (How it works)
   -------------------------------------------------------------------------- */
.steps { display: grid; gap: var(--s-3); grid-template-columns: 1fr; counter-reset: step; }
@media (min-width: 768px) { .steps { grid-template-columns: repeat(3, 1fr); } }
.step {
  background: var(--pf-white); border: 1px solid var(--pf-border);
  border-radius: var(--r-card); padding: var(--s-4);
}
.step::before {
  counter-increment: step; content: counter(step, decimal-leading-zero);
  display: block; font-weight: 800; font-size: var(--fs-small);
  color: var(--pf-orange); margin-bottom: var(--s-2); letter-spacing: 0.04em;
}
.step h3 { font-size: 19px; margin-bottom: var(--s-1); }
.step p { font-size: 15px; line-height: 1.55; color: var(--pf-grey-600); }

/* --------------------------------------------------------------------------
   Trust strip
   -------------------------------------------------------------------------- */
.trust-strip {
  display: grid; gap: var(--s-2); grid-template-columns: 1fr;
  padding-block: var(--s-6);
}
@media (min-width: 480px) { .trust-strip { grid-template-columns: repeat(2, 1fr); gap: var(--s-3); } }
@media (min-width: 768px) { .trust-strip { padding-block: var(--s-8); } }
@media (min-width: 768px) { .trust-strip { grid-template-columns: repeat(4, 1fr); } }
.trust-item { display: flex; align-items: center; gap: var(--s-2); font-weight: 600; font-size: 15px; }
.trust-item svg { flex: 0 0 auto; color: var(--pf-green); }

/* --------------------------------------------------------------------------
   Feature rows / value props
   -------------------------------------------------------------------------- */
.value-grid { display: grid; gap: var(--s-3); grid-template-columns: 1fr; }
@media (min-width: 768px) { .value-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .value-grid--4 { grid-template-columns: repeat(4, 1fr); } }
.value-card {
  background: var(--pf-white); border: 1px solid var(--pf-border);
  border-radius: var(--r-card); padding: var(--s-4);
}
.value-card h3 { font-size: 18px; margin-bottom: var(--s-1); }
.value-card p { font-size: 15px; line-height: 1.55; color: var(--pf-grey-600); }

/* --------------------------------------------------------------------------
   Widgets shared (suggest / slots / solo config / waitlist)
   -------------------------------------------------------------------------- */
.widget {
  background: var(--pf-white); border: 1px solid var(--pf-border);
  border-radius: var(--r-card); padding: var(--s-4);
}
.field { margin-bottom: var(--s-2); }
.field label { display: block; font-size: var(--fs-small); font-weight: 600; margin-bottom: var(--s-1); }
.field input, .field select, .field textarea {
  width: 100%; padding: 12px 14px;
  font: 400 16px/1.4 Inter, system-ui, sans-serif; color: var(--pf-black);
  background: var(--pf-white); border: 1px solid var(--pf-border);
  border-radius: 10px; outline: none;
  transition: border-color 0.15s ease;
}
/* THE RING HAS TO SURVIVE THE RESET ABOVE (2026-08-12). `.field input` is
   (0,1,1) and the global :focus-visible ring is (0,1,0), so the reset won and
   these controls showed NOTHING on keyboard focus. Measured by tabbing the
   deployed page: 24 of 26 stops drew the orange ring, and the 2 that did not
   were both .field controls — the postcode input and the brief textarea. This
   also makes the aria-invalid rule further down live: it sets outline-color,
   which does nothing while outline-style is none. */
.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible {
  outline: 2px solid var(--pf-orange);
  outline-offset: 2px;
}
.field textarea { resize: vertical; min-height: 64px; }
/* native selects: house chevron (same family as the disclosure), 14px inset,
   decorative grey. -webkit-appearance for iOS Safari; explicit padding-right
   keeps text clear of the icon (appearance:none quirk). */
select { -webkit-appearance: none; -moz-appearance: none; appearance: none; }
select, .field select {
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path d="M2 4l4 4 4-4" stroke="%239A9A9A" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>');
  background-repeat: no-repeat; background-position: right 14px center; background-size: 12px 12px;
  padding-right: 40px;
}
select::-ms-expand { display: none; }
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--pf-grey-400); }
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--pf-orange); }
.seg { display: flex; gap: var(--s-1); flex-wrap: wrap; }
/* CONTROLS RESIZE TO FIT THEIR BOX (Jon-ruled 2026-08-10, containment):
   a 96px floor on three tiles demanded 304px, and the delivery column is
   narrower than that once postcode and homes share a row — so "2,500 /
   5,000 / 10,000" rendered outside its box. A segment shrinks with its
   container now; it never sets a floor its parent cannot honour. */
.seg button {
  flex: 1 1 0; min-width: 0; padding: 10px 6px;
  border: 1px solid var(--pf-border); background: var(--pf-white); border-radius: 10px;
  font: 600 15px Inter, system-ui, sans-serif; cursor: pointer; color: var(--pf-black);
  transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}
.seg button:hover { border-color: var(--pf-grey-400); }
.seg button:active { background: var(--pf-grey-50); }
.seg button[aria-pressed="true"] { border-color: var(--pf-orange); background: var(--pf-orange-tint); color: var(--pf-orange); }
.hint { font-size: var(--fs-small); line-height: var(--lh-small); color: var(--pf-grey-600); margin-top: var(--s-1); }

.notice { font-size: 15px; line-height: 1.5; padding: var(--s-2); border-radius: 10px; }
.notice--ok { background: rgba(20, 53, 40, 0.08); color: var(--pf-green); border: 1px solid rgba(20, 53, 40, 0.28); }
.notice--err { background: var(--pf-orange-tint); color: #C22C00; border: 1px solid rgba(253, 54, 1, 0.25); }

.spinner {
  width: 28px; height: 28px; margin: 0 auto var(--s-2);
  border: 3px solid var(--pf-border); border-top-color: var(--pf-orange);
  border-radius: 50%; animation: pf-spin 1s linear infinite;
}
@keyframes pf-spin { to { transform: rotate(360deg); } }
.loading-block { text-align: center; padding: var(--s-4) 0; color: var(--pf-grey-600); }

/* Suggest tool */
.suggest-grid { display: grid; gap: var(--s-3); grid-template-columns: 1fr; }
@media (min-width: 900px) { .suggest-grid { grid-template-columns: 1fr 1fr; align-items: start; } }
.suggest-result { min-height: 320px; display: flex; flex-direction: column; justify-content: center; }
.suggest-result .kicker { font-size: 12px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--pf-orange); margin-bottom: var(--s-1); }
.suggest-result h3 { font-size: var(--fs-h3); margin-bottom: var(--s-2); }
.suggest-result .areas { color: var(--pf-grey-600); font-size: var(--fs-small); margin-top: var(--s-1); }
.suggest-result .actions { margin-top: var(--s-3); display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap; }

/* Slots availability */
.slots-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-2); margin-bottom: var(--s-2); flex-wrap: wrap; }
.slots-remaining { font-weight: 800; font-size: var(--fs-h3); letter-spacing: -0.4px; }
.slots-remaining b { color: var(--pf-orange); }
.slots-grid { display: grid; gap: var(--s-1); grid-template-columns: 1fr; list-style: none; }
@media (min-width: 560px) { .slots-grid { grid-template-columns: 1fr 1fr; } }
.slot-cat {
  display: flex; align-items: center; gap: var(--s-1);
  padding: 12px 14px; border: 1px solid var(--pf-border); border-radius: 10px;
  font-size: 15px; font-weight: 600; background: var(--pf-white);
}
.slot-cat .ic { width: 20px; text-align: center; }
.slot-cat.avail .ic { color: var(--pf-green); }
.slot-cat.taken { color: var(--pf-grey-600); background: var(--pf-grey-50); }
.slot-cat.taken .name { text-decoration: line-through; }

/* Price panel (solo config + shared price block) */
.price-panel {
  background: var(--pf-grey-50); border: 1px solid var(--pf-border);
  border-radius: var(--r-card); padding: var(--s-4); margin-top: var(--s-2);
}
.price-panel .context { font-size: var(--fs-small); font-weight: 600; color: var(--pf-grey-600); }
.price-panel .amount { font-weight: 800; font-size: clamp(28px, 2vw + 20px, 36px); letter-spacing: -0.6px; margin-top: 4px; }
.price-panel .saving { font-size: var(--fs-small); font-weight: 700; color: var(--pf-green); margin-top: 4px; min-height: var(--lh-small); }
.price-panel .scarcity { font-size: 15px; font-weight: 600; color: var(--pf-orange); margin-top: var(--s-1); min-height: 24px; }

/* FAQ */
.faq { border-top: 1px solid var(--pf-border); }
.faq details { border-bottom: 1px solid var(--pf-border); }
.faq summary {
  cursor: pointer; list-style: none; display: flex; justify-content: space-between; gap: var(--s-2);
  padding: var(--s-3) 0; font-weight: 700; font-size: var(--fs-body);
  transition: color 0.15s ease;
}
.faq summary:hover { color: var(--pf-orange); }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; color: var(--pf-orange); font-weight: 700; }
.faq details[open] summary::after { content: "–"; }
.faq details p { padding-bottom: var(--s-3); color: var(--pf-grey-600); }

/* CTA band */
.cta-band { text-align: center; }
.cta-band .hero-ctas { justify-content: center; }

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */
.site-footer { border-top: 1px solid var(--pf-border); background: var(--pf-white); }
.footer-grid {
  display: grid; gap: var(--s-6); padding-block: var(--s-8);
  grid-template-columns: 1fr;
}
@media (min-width: 768px) { .footer-grid { grid-template-columns: 2fr 1fr 1fr 1fr; } }
.footer-brand img { width: 180px; }
.footer-brand p { margin-top: var(--s-2); font-size: var(--fs-small); line-height: var(--lh-small); color: var(--pf-grey-600); }
.footer-col .footer-h { font-size: var(--fs-small); text-transform: uppercase; letter-spacing: 0.05em; color: var(--pf-grey-600); margin-bottom: var(--s-2); font-weight: 700; }
.footer-col ul { list-style: none; }
.footer-col a {
  font-size: 15px; text-decoration: none; color: var(--pf-black);
  display: inline-block; padding-block: 12px; /* ≥44px touch target */
}
.footer-col a:hover { color: var(--pf-orange); }
.footer-legal a { display: inline-block; padding-block: 12px; }
.footer-legal {
  display: flex; flex-wrap: wrap; gap: var(--s-3); justify-content: space-between; align-items: center;
  border-top: 1px solid var(--pf-border); padding-block: var(--s-3);
  font-size: var(--fs-small); color: var(--pf-grey-600);
}

/* --------------------------------------------------------------------------
   Reveal on scroll — ADDITIVE only: content is never hidden. The entrance
   animation plays when the observer adds .in; if JS/IO never runs, everything
   is simply visible. (Global reduced-motion rule disables the animation.)
   -------------------------------------------------------------------------- */
/* NO `backwards` — see the note on `.reveal-art`. A fill mode that holds a
   zero-opacity first frame makes the animation, rather than the content, the
   reason something is on screen; when the browser never starts it the element
   is invisible for ever. The `to` is explicit so the end state is stated
   rather than inferred from the element's own style. */
.js .reveal.in { animation: pf-rise 0.5s ease; }
@keyframes pf-rise {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
   Page-specific helpers
   -------------------------------------------------------------------------- */
.annotation-list { list-style: none; display: grid; gap: var(--s-1); margin-top: var(--s-3); }
.annotation-list li { display: flex; gap: var(--s-1); align-items: baseline; font-size: 15px; color: var(--pf-grey-600); }
.annotation-list li::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--pf-orange); flex: 0 0 auto; transform: translateY(-1px); }

.two-col { display: grid; gap: var(--s-6); grid-template-columns: 1fr; align-items: center; }
@media (min-width: 900px) { .two-col { grid-template-columns: 1fr 1fr; } }
.two-col--top { align-items: start; }

.option-cards { display: grid; gap: var(--s-3); grid-template-columns: 1fr; }
@media (min-width: 768px) { .option-cards { grid-template-columns: 1fr 1fr; } }
.option-card {
  background: var(--pf-white); border: 1px solid var(--pf-border);
  border-radius: var(--r-card); padding: var(--s-4);
  display: flex; flex-direction: column; gap: var(--s-2);
}
.option-card .tag { font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--pf-grey-600); }
.option-card p { font-size: 15px; line-height: 1.55; color: var(--pf-grey-600); }
/* price + CTA pin to the bottom so the two option cards align with each other */
.option-card .price { margin-top: auto; font-size: var(--fs-body); color: var(--pf-black); }

.legal-body { max-width: 70ch; }
.legal-body h2 { font-size: var(--fs-h3); margin-top: var(--s-6); margin-bottom: var(--s-2); }
.legal-body p { margin-bottom: var(--s-2); color: var(--pf-grey-600); }

.error-hero { text-align: center; padding-block: var(--s-16); }

/* ==========================================================================
   VISUAL-FIRST LAYER — composed card, designer chrome, skeletons, motion
   All transitions 150–250ms ease-out. prefers-reduced-motion kills all of it
   via the global rule at the top of this file.
   ========================================================================== */

:root {
  --ease: cubic-bezier(0.25, 0.6, 0.35, 1);
  --t-fast: 180ms;
  --t-slow: 240ms;
  --shadow-print: 0 12px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.05);
}

/* ---- Composed 8-slot card artefact (the site's hero object) ----
   LIVING-CARD-01 (decisions v3 s1): PHOTOREAL ALWAYS-ON — printed-object
   treatment at the STRONG intensity: paper edge, texture, deep soft shadow,
   slight perspective on the OUTER wrapper (the 3D flip inner is untouched,
   and live typing only re-renders slot content — the chrome is static CSS). */
[data-composed-card] { transform: perspective(1600px) rotateX(1.4deg); transform-origin: 50% 60%; }
#dk-peek [data-composed-card], .composed-card--mini { transform: none; }
.composed-card {
  background: var(--pf-white); border: 1px solid var(--pf-border);
  border-radius: 6px; overflow: hidden;
  box-shadow: 0 30px 58px rgba(0, 0, 0, 0.24), 0 6px 16px rgba(0, 0, 0, 0.12);
  border-bottom: 4px solid rgba(0, 0, 0, 0.15);
  position: relative;
}
.composed-card::after {
  /* CARD-FACE-02: grain dialled to PAPER — fine turbulence, low contrast,
     warm; reads as print stock at stage and lightbox sizes */
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 9;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.86' numOctaves='2' seed='7'/%3E%3CfeColorMatrix values='0 0 0 0 0.55 0 0 0 0 0.50 0 0 0 0 0.42 0 0 0 0.5 0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23p)'/%3E%3C/svg%3E");
  background-size: 140px 140px;
  opacity: 0.16; mix-blend-mode: multiply;
}
/* CARD-FACE-01: the face design system. The wordmark stays typographic;
   ORANGE is the frame accent (rules/keylines) so the ads keep the colour
   stage. LOOK-RESTORE-02 s3 (Jon-ratified): the FULL-COLOUR ORANGE mark is
   permitted on the masthead — brand presence beats strict mono; body and
   panels stay clean. */
/* FACE-CALM-01 (Jon-ratified): QUIET MASTHEAD — understated, classy, eyes
   on the ads. Mark + wordmark at modest size, ONE refined letterspaced
   small-caps line. No display-sized area name. Geometry is FIXED: the band
   height never responds to content — the line scales/tracks to fit any
   area-name length (site.js fitMastLine). The traced-SVG mark stays crisp
   at print-zoom; orange default, ?face=mono previews mono-white. */
.cc-mast .m-quiet { display: flex; align-items: center; gap: 0.9em; min-width: 0; }
.cc-mast .m-mark { height: 2.1em; width: auto; flex: none; filter: none; opacity: 1; }
[data-face="mono"] .cc-mast .m-mark { filter: brightness(0) invert(1); opacity: 0.95; }
[data-face="b"] .cc-mast .m-mark { filter: brightness(0); }
.cc-mast .m-brand { font-size: 0.82em; font-weight: 800; letter-spacing: 0.02em; flex: none; }
.cc-mast .m-brand b { color: inherit; }
.cc-mast .m-line {
  margin-left: auto; min-width: 0; overflow: hidden; white-space: nowrap;
  font-size: 0.62em; font-weight: 600; letter-spacing: 0.26em;
  text-transform: uppercase; opacity: 0.9; text-align: right;
}
/* frame rules: orange bounds the body; consistent panel keylines + gutters */
.composed-card .cc-mast { border-bottom: 3px solid var(--pf-brand); }
.composed-card .cc-band { border-top: 3px solid var(--pf-brand); }
.composed-card .cc-panel { border: 1px solid #E2DDD5; border-radius: 3px; }
/* ---- FACE-ASSET-01 s1: THE DESIGNED FRAME LAYER ----
   Jon's frame master (downscale-only web derivative) carries the bands,
   texture and rules; the DOM carries lockup text, slot content and the QR
   on top. Active when the face manifest loads (html[data-frame], set by
   site.js) — the CSS-drawn bands above remain ONLY as the no-manifest
   fallback. Band geometry from the master's pixel scan: header 13.99%,
   footer 7.2%. Scales with the card at every size; the 1966x2789 asset
   covers 3x lightbox zoom without upscaling. */
html[data-frame] .composed-card { --cc-foot-h: 40px; position: relative; background: #fff; }
/* the frame paints over the CARD PROPER only — the cc-foot (Side A label +
   flip control) is screen chrome inside the same element, never print */
html[data-frame] .composed-card::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; bottom: var(--cc-foot-h);
  background: var(--pf-frame) center / 100% 100% no-repeat;
}
html[data-frame] .composed-card > * { position: relative; z-index: 1; }
html[data-frame] .composed-card .cc-mast {
  background: transparent; border-bottom: 0;
  height: calc((100% - var(--cc-foot-h)) * 0.1399); box-sizing: border-box;
  display: flex; flex-direction: column; justify-content: center;
}
/* the kicker's tight line-height clipped its cap-tops in the fixed band */
html[data-frame] .composed-card .cc-mast .m-line, html[data-frame] .composed-card .cc-mast .m-brand { line-height: 1.4; }
html[data-frame] .composed-card .cc-band {
  background: transparent; border-top: 0;
  height: calc((100% - var(--cc-foot-h)) * 0.072); box-sizing: border-box;
  display: flex; align-items: center; overflow: hidden;
}
/* the recruitment line lives INSIDE the fixed band — clamp, never spill */
html[data-frame] .composed-card .cc-band .b-line { font-size: 9.5px; line-height: 1.25; }
html[data-frame] .composed-card .cc-grid { background: transparent; }
html[data-frame] .composed-card .cc-foot { height: var(--cc-foot-h); box-sizing: border-box; }
html[data-frame] .composed-card::after { content: none; } /* grain is baked in the asset */
html[data-frame] .b-qr:not([src]), html[data-frame] .b-qr[src=""] { visibility: hidden; }
html[data-frame] .composed-card--mini { --cc-foot-h: 0px; } /* minis carry no foot */
/* footer QR (print-ready recruitment) */
.cc-band .b-qr { height: 78%; max-height: 44px; aspect-ratio: 1; width: auto; background: #fff; padding: 3px; border-radius: 3px; flex: none; }
/* ---- THE THREE CANDIDATES (gate 1 — Jon picks; A is the default frame) ----
   A (default): bold black mast + band, white body, orange rules
   B: paper mast, strong orange rules, mono black wordmark
   C: orange mast (mono-reversed lockup), black band */
[data-face="b"] .composed-card .cc-mast { background: #F6F3EE; color: var(--pf-black); border-top: 6px solid var(--pf-brand); }
[data-face="c"] .composed-card .cc-mast { background: var(--pf-brand); color: #fff; border-bottom: 3px solid var(--pf-black); }
[data-face="c"] .composed-card .cc-band { background: #000; border-top: 3px solid var(--pf-brand); }
[data-face="c"] .composed-card .cc-panel { border-color: #E8E2D9; }
/* branded footer strip (s7): part of the card artwork, print master included */
.cc-band {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-2);
  padding: 5px var(--s-3); background: var(--pf-black); color: var(--pf-white);
  font-size: 10.5px; font-weight: 600; min-height: 6.7%;
}
.cc-band .b-pf { font-weight: 800; white-space: nowrap; }
.cc-band .b-pf b { color: var(--pf-orange); }
.cc-band .b-line { opacity: 0.92; text-align: right; }
/* decision block line: one unit with the card (s4); at rail width it takes
   the closes row's slot so the equal-height composition is untouched */
.decision-line { margin: 2px 0 0; font-weight: 600; min-height: 18px; }
.decision-line .dec-sep { margin: 0 6px; color: var(--pf-grey-400, #9A948C); }
#dk-area-districts { min-height: 16px; margin-top: 4px; }
@media (max-width: 430px) { #dk-area { font-size: 13.5px; } }
/* CARD LIGHTBOX (CARD-PORTRAIT-01 fix): FIT-TO-VIEW initial state — the card
   sizes by viewport height AND width, whichever binds; pan is clamped in JS
   so the whole card stays reachable at every zoom level */
/* LOOK-RESTORE-02 s2: fit-to-view by BOTH axes — the portrait card was
   sized by height alone and overflowed the width on phones (clipped
   left/right); the width term caps it so the whole card is visible on open */
.lightbox-body [data-composed-card] { height: min(88vh, 1500px, calc((100vw - 48px) * 1.4189)); width: auto; display: flex; justify-content: center; transform: none; }
.lightbox-body [data-composed-card] .cc3d, .lightbox-body [data-composed-card] .cc3d-inner { height: 100%; }
.lightbox-body [data-composed-card] .composed-card { height: 100%; width: auto; max-width: 92vw; }
.composed-card .cc-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-2);
  padding: var(--s-2) var(--s-3); background: var(--pf-black); color: var(--pf-white);
}
.composed-card .cc-head .cc-title { font-weight: 800; font-size: var(--fs-small); letter-spacing: 0.08em; }
.composed-card .cc-head .cc-brand { font-weight: 800; font-size: var(--fs-small); }
.composed-card .cc-head .cc-brand b { color: var(--pf-orange); }
.composed-card .cc-side {
  padding: 6px var(--s-3); font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--pf-grey-600); background: var(--pf-grey-50);
  border-block: 1px solid var(--pf-border);
}
.composed-card .cc-side + .cc-grid { padding-top: var(--s-2); }
.composed-card { aspect-ratio: 148 / 210; display: flex; flex-direction: column; }
.composed-card .cc-grid { display: grid; grid-template-columns: 1fr 1fr; grid-auto-rows: 1fr; gap: var(--s-1); padding: var(--s-2); flex: 1; min-height: 0; }
.composed-card .cc-panel {
  position: relative; border-radius: var(--s-1); min-height: 0;
  overflow: hidden; border: 1px solid var(--pf-border); background: var(--pf-grey-50);
}
.composed-card .cc-panel img { width: 100%; height: 100%; object-fit: cover; display: block; }
.composed-card .cc-panel.your-space {
  /* CARD-FACE-01 invitation pass: double-rule orange frame + display caps —
     consistent with the face system, unmistakable without shouting */
  border: 1.5px solid var(--pf-orange); background: linear-gradient(160deg, #FFFFFF, var(--pf-orange-tint));
  box-shadow: inset 0 0 0 4px #fff, inset 0 0 0 5.5px rgba(253, 54, 1, 0.45);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; text-align: center; padding: var(--s-1); overflow: hidden;
}
.composed-card .cc-panel.your-space .ys-label { text-transform: uppercase; letter-spacing: 0.01em; transform: scaleX(0.95); }
/* Orange text stays ≥19px bold (AA large-text); the sub line goes black so it
   can be small. Chip is green-on-tint (AA at any size). */
.composed-card .cc-panel.your-space .ys-label { font-weight: 800; font-size: 19px; line-height: 1.1; letter-spacing: 0; color: var(--pf-orange); }
.composed-card .cc-panel.your-space .ys-sub { font-weight: 700; font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--pf-black); }
.composed-card .cc-panel.your-space .chip { padding: 5px 10px; font-size: 12px; }
.composed-card .cc-panel.your-space .pf-counter { font-size: 12px; }
@media (max-width: 1099px) and (min-width: 900px) {
  .composed-card .cc-panel.your-space .ys-sub { display: none; } /* narrow hero column */
}
.composed-card .cc-foot {
  padding: 10px var(--s-3); background: var(--pf-grey-50); border-top: 1px solid var(--pf-border);
  font-size: 12px; line-height: var(--lh-small); font-weight: 600; color: var(--pf-grey-600);
  display: flex; justify-content: space-between; gap: var(--s-2); flex-wrap: wrap;
}
.composed-card--mini .cc-head { padding: 10px var(--s-2); }
.composed-card--mini .cc-foot { display: none; }

/* FACE-CALM-01: the composite pipeline is retired (decisions log) — the eye
   opens the static marketing shot in the main lightbox. Stage positioning +
   the eye button remain. */
.stage-content[data-pv="solo"] { position: relative; }
.stage-content[data-pv="card"] { position: relative; }
.stage-content[data-pv="card"] .lbx-btn, .stage-content[data-pv="solo"] .lbx-btn { right: 8px; top: 44px; }
/* the IRL shot honest caption (never the customer's own design) */
.lb-shot-cap { text-align: center; margin: 10px 0 0; font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.92); }
.designer-preview .lbx-btn { right: 8px; top: 44px; }
/* counts-only truth panel: taken, kind honestly unknown */
.composed-card .cc-panel.lc-taken { background: #EFEBE4; display: flex; align-items: center; justify-content: center; }
.composed-card .lc-taken .lc-chip { font-size: 11px; font-weight: 800; background: var(--pf-white); border: 1px solid var(--pf-border); border-radius: var(--r-pill); padding: 4px 10px; }

/* ---- TAXONOMY-01: grouped searchable trade picker ---- */
.cat-pick { position: relative; }
.cat-list {
  position: absolute; z-index: 60; left: 0; right: 0; top: calc(100% + 4px);
  background: var(--pf-white); border: 1px solid var(--pf-border); border-radius: 10px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.14); max-height: 300px; overflow-y: auto; padding: 4px;
}
.cat-list .cg { font-size: 10.5px; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; color: var(--pf-grey-600); padding: 8px 10px 3px; }
.cat-list .ci {
  display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left;
  border: 0; background: none; padding: 7px 10px; border-radius: 6px; cursor: pointer;
  font: 500 14px/1.2 Inter, system-ui, sans-serif; color: var(--pf-black);
}
.cat-list .ci:hover, .cat-list .ci:focus-visible, .cat-list .ci[aria-selected="true"] { background: var(--pf-orange-tint); }
.cat-list .ci .st { margin-left: auto; font-size: 11px; font-weight: 700; color: var(--pf-grey-600); white-space: nowrap; }
.cat-list .ci[data-taken] { color: var(--pf-grey-400, #9A948C); cursor: not-allowed; }
.cat-list .ci[data-taken] .st { color: var(--pf-brand); }
.cat-list .dym { padding: 8px 10px 2px; font-size: 12px; color: var(--pf-grey-600); }
.cat-list .ci.other { border-top: 1px solid var(--pf-border); border-radius: 0 0 6px 6px; color: var(--pf-grey-600); font-weight: 600; }
#dk-cat-avail { min-height: 18px; margin-top: 4px; }
#dk-cat-avail[data-state="taken"] { color: var(--pf-brand); font-weight: 600; }
#dk-cat-avail[data-state="reserved"] { color: var(--pf-grey-600); font-weight: 600; }
/* item 2: slot-mode CTA — selector hidden, one quiet line holds the space */
#dz-cta[hidden] { display: none; }
/* ---- LIVING-CARD-02: format tiles (s9), flick (s8), live-claim polish ---- */
.fmt-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; min-height: 92px; }
.fmt-tile {
  border: 2px solid var(--pf-border); border-radius: 10px; background: var(--pf-white);
  padding: 8px; cursor: pointer; text-align: left; display: flex; flex-direction: column; gap: 6px;
  font: 600 13px/1.2 Inter, system-ui, sans-serif; color: var(--pf-black);
  transition: border-color 0.15s ease-out;
}
.fmt-tile img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; border-radius: 0; /* paper ruling */ display: block; background: var(--pf-grey-50); }
.fmt-tile[aria-pressed="true"] { border-color: var(--pf-brand); }
.fmt-tile:hover, .fmt-tile:focus-visible { border-color: var(--pf-brand); }
.fmt-tile[data-soon] { opacity: 0.75; }
.dz-flick { display: flex; justify-content: center; gap: 6px; min-height: 26px; }
.dz-flick button {
  border: 1px solid var(--pf-border); background: var(--pf-white); border-radius: var(--r-btn);
  padding: 3px 12px; font: 600 12px/1.4 Inter, system-ui, sans-serif; color: var(--pf-grey-600); cursor: pointer;
}
.dz-flick button[aria-pressed="true"] { background: var(--pf-black); border-color: var(--pf-black); color: var(--pf-white); }
.designer-preview .dz-ctx {
  position: absolute; inset: 0; z-index: 5; overflow: hidden;
  background: var(--pf-grey-50); display: flex; align-items: center; justify-content: center; padding: 4%;
}
/* STAGE-8 ROOT CAUSE of the whole blank-stage saga: display:flex above beats
   the UA's [hidden]{display:none} — the "hidden" context was an always-on
   grey panel silently covering the stage since the flick shipped. Never let
   a class display defeat the hidden attribute here. */
.designer-preview .dz-ctx[hidden] { display: none; }
/* …and the GLOBAL guard, ending the whole bug class: five prior spot-fixes
   (#dz-cta, .testimonials, .expand-btn, ys-placeholder/live, advisor faces)
   were this same conflict. The hidden attribute always wins, everywhere. */
[hidden] { display: none !important; }
.designer-preview .dz-ctx [data-composed-card] { width: 100%; max-width: 640px; }
/* founding invitation: showcase-quality open slot — never a labelled box */
button.cc-panel.lc-claim {
  background: linear-gradient(155deg, #FFFFFF, var(--pf-orange-tint) 70%);
  border: 2px dashed var(--pf-orange);
}
button.cc-panel.lc-claim .ys-name-live { font-weight: 800; font-size: 16px; line-height: 1.15; color: var(--pf-black); overflow-wrap: anywhere; }
button.cc-panel.lc-claim .ys-cat-live { font-size: 10.5px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--pf-grey-600); }
button.cc-panel.lc-claim.claiming { border-style: solid; }
/* claimant view: their business, unblurred, quietly confirmed */
.composed-card .lc-mine { background: linear-gradient(160deg, #FFFFFF, #F4F1EC); }
.composed-card .lc-mine .lc-biz { font-weight: 800; font-size: 16px; line-height: 1.15; }
.composed-card .lc-mine .lc-cat { font-size: 11px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--pf-grey-600); }
/* (solo letterbox rules replaced by COMPOSITE REALISM V2; the .solo-frame
   span retired with blank-flyer.png — the pane hosts the canonical render) */

/* ---- DESIGNER-03: the Brief (three steps, fixed geometry) ---- */
/* steps stack in ONE grid cell - the tallest step sizes the pane, so moving
   between steps never shifts the stage (fixed-stage law) */
.dz-stephead { display: flex; align-items: center; gap: 10px; min-height: 24px; margin-bottom: 6px; position: relative; }
.dz-steps { display: grid; }
.dz-step { grid-area: 1 / 1; min-width: 0; }
.dz-step[data-off] { visibility: hidden; pointer-events: none; }
/* .dz-i keeps its class for the tooltip wiring only; all styling comes from
   the .info-i house component (DETAIL-07) */
#pf-designer-form { position: relative; }
.dz-pop { max-width: 280px; }
.seg--wrap { flex-wrap: wrap; }
.seg--wrap button { flex: 1 1 45%; }
.dz-swatches { display: flex; align-items: center; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
.dz-swatches .sw { width: 26px; height: 26px; border-radius: 6px; border: 2px solid var(--pf-border); cursor: pointer; padding: 0; }
.dz-swatches .sw[aria-pressed="true"] { border-color: var(--pf-black); box-shadow: 0 0 0 2px #fff inset; }
.dz-swatches input[type="color"] { width: 30px; height: 30px; border: 1px solid var(--pf-border); border-radius: 6px; background: none; padding: 2px; cursor: pointer; }
.dz-thumbs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px; }
.dz-thumbs:empty { display: none; }
.dz-thumb { position: relative; aspect-ratio: 4 / 3; border-radius: 0; /* paper ruling */ overflow: hidden; border: 1px solid var(--pf-border); }
.dz-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.dz-thumb .tstar, .dz-thumb .tdel {
  position: absolute; top: 3px; width: 22px; height: 22px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.94); border: 1px solid var(--pf-border);
  font-size: 12px; line-height: 20px; text-align: center; cursor: pointer; padding: 0;
}
.dz-thumb .tstar { left: 3px; width: 26px; height: 26px; font-size: 14px; line-height: 24px; }
.dz-thumb .tdel { right: 3px; }
/* DESIGNER-FIT §5: star state obvious; tiles roomy in their outlines at 390 */
.dz-thumb.is-hero .tstar { background: var(--pf-brand); color: var(--pf-white); border-color: var(--pf-brand); }
@media (max-width: 480px) { .dz-thumbs { grid-template-columns: repeat(2, 1fr); gap: 10px; } }
/* DESIGNER-GENERATION fix D (2026-07-18): the hero ring keeps the SAME
   geometry as unstarred thumbs (1px border + 1px inset ring = 2px visual) —
   the old border-width jump (1px→2px) shifted the image against its outline */
.dz-thumb.is-hero { border-color: var(--pf-brand); box-shadow: inset 0 0 0 1px var(--pf-brand); }
.dz-thumb.is-hero .tstar { color: var(--pf-brand); }
.dz-thumb .tres { position: absolute; left: 0; right: 0; bottom: 0; font-size: 9px; background: rgba(26,26,26,.75); color: #fff; padding: 2px 4px; }
.dz-rights { font-size: 11px; }
/* DESIGNER-03 presentation pass: MEDIUM print-object level ships as default
   (Jon picks from the before/after strip) - visible paper edge, deeper soft
   shadow, perceptible texture at stage size */
.artefact-stage .designer-preview, .lightbox .designer-preview {
  box-shadow: 0 22px 48px rgba(0, 0, 0, 0.17), 0 4px 12px rgba(0, 0, 0, 0.08);
  border-bottom: 3px solid rgba(0, 0, 0, 0.10);
}
.designer-preview::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: repeating-linear-gradient(115deg, rgba(255,255,255,0.022) 0 2px, rgba(0,0,0,0.014) 2px 4px);
}
[data-print-level="current"] .designer-preview { box-shadow: var(--shadow-print); border-bottom: 1px solid var(--pf-border); }
[data-print-level="current"] .designer-preview::after { display: none; }
[data-print-level="strong"] .designer-preview {
  box-shadow: 0 30px 60px rgba(0, 0, 0, 0.24), 0 6px 16px rgba(0, 0, 0, 0.12);
  border-bottom: 5px solid rgba(0, 0, 0, 0.16);
}
[data-print-level="strong"] .designer-preview::after {
  background: repeating-linear-gradient(115deg, rgba(255,255,255,0.05) 0 2px, rgba(0,0,0,0.03) 2px 4px);
}

/* ---- Browser-chrome frame (AI designer) ---- */
.chrome {
  background: var(--pf-white); border: 1px solid var(--pf-border);
  border-radius: var(--r-card); overflow: hidden; box-shadow: var(--shadow-print);
}
.chrome .chrome-bar {
  display: flex; align-items: center; gap: var(--s-1);
  padding: 10px var(--s-2); border-bottom: 1px solid var(--pf-border); background: var(--pf-grey-50);
}
.chrome .chrome-bar .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--pf-border); }
.chrome .chrome-bar .chrome-title { margin-left: var(--s-1); font-size: 12px; font-weight: 700; color: var(--pf-grey-600); letter-spacing: 0.03em; }
.chrome .chrome-body { padding: var(--s-3); }
.designer-preview { position: relative; border-radius: 0; /* paper ruling */ overflow: hidden; border: 1px solid var(--pf-border); aspect-ratio: var(--pf-cell-ratio-w) / var(--pf-cell-ratio-h); background: var(--pf-grey-50); }
.designer-preview img { width: 100%; height: 100%; object-fit: cover; display: block; position: relative; z-index: 2; }
.designer-preview .dp-status {
  position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: var(--s-2); background: rgba(255, 255, 255, 0.92);
  font-weight: 600; color: var(--pf-grey-600); text-align: center; padding: var(--s-2);
}
/* DECORATION MAY NEVER BE WHAT MAKES CONTENT VISIBLE (measured 2026-08-26).
   This carried `backwards`, so the element took the keyframe's `from` state —
   opacity 0 — before the animation began. When the class is applied while the
   image is not being rendered (the mode switch clears the stage a frame
   earlier), the browser never STARTS the animation: `getAnimations()` returns
   an empty list, and `backwards` then pins the invisible first frame for ever.
   Measured on the deployed deck: the customer's artwork loaded at 2552x3579,
   sized 405x568, in the viewport, computed opacity 0 — a blank flyer.
   REMOVING `backwards` WAS NOT ENOUGH, and the second measurement is the one
   that matters: with `fill: none` and `playState: idle`, Chrome STILL applied
   the keyframe's first frame, and an explicit `.pff-art { opacity: 1 }` lost to
   it — animations outrank normal declarations in the cascade. Only
   `animation: none` restored it. So a stuck animation hides content whatever
   its fill mode, and the rule has to be stronger than a fill-mode choice:
   THE ARTWORK'S REVEAL CARRIES NO OPACITY AT ALL. A frame that sticks now
   costs three per cent of scale instead of the entire picture.
   `pf-rise` keeps its fade because its class is added by an intersection
   observer — an element must be rendered to be observed intersecting, so the
   animation is guaranteed to start. `pf-art` has no such guarantee: it is
   applied programmatically to an image the mode switch may have just hidden. */
.reveal-art { animation: pf-art var(--t-slow) var(--ease); }
@keyframes pf-art { from { transform: scale(0.97); } to { transform: none; } }

/* ---- Skeleton shimmer (exact-size placeholders => zero layout shift) ---- */
.skel {
  position: relative; overflow: hidden; background: var(--pf-grey-50);
  border-radius: var(--s-1); border: 1px solid var(--pf-border); color: transparent !important;
}
.skel::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent 25%, rgba(255, 255, 255, 0.85) 50%, transparent 75%);
  background-size: 200% 100%; animation: pf-skel 1.2s linear infinite;
}
@keyframes pf-skel { to { background-position: -200% 0; } }

/* ---- Status chips ---- */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-small); font-weight: 700; line-height: 1;
  border-radius: var(--r-pill); padding: 8px 14px; white-space: nowrap;
}
.chip::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
/* `.chip--open` STAYS, and I removed it once on 2026-08-29 before checking.
   MOBILE-TILE-01 retired the green-dot pill AS A SLOT COUNTER, and names its
   survivors in the same breath: "The pill classes survive ONLY for non-counter
   states: the amber degraded chip (chip--warn), chip--sold, and the /shared
   category ✓ example chips (the locked category ✓/🔒 display is not a
   counter)." `site/shared/index.html` uses this class for exactly those ✓
   chips, so deleting the rule takes their colour off a live page.
   THE LESSON IS THE AUDIT'S OWN: a retirement is scoped, and "the token is in
   the build" is not the same claim as "the retired thing is in the build".
   I checked site/index.html and not the other served pages. */
.chip--open { color: var(--pf-green); background: rgba(20, 53, 40, 0.08); }
.chip--low { color: #C22C00; background: var(--pf-orange-tint); }
.chip--sold { color: var(--pf-grey-600); background: var(--pf-grey-50); border: 1px solid var(--pf-border); }
/* MOBILE-TILE-01: ONE house counter for every slot-availability figure —
   the decision block's orange segmented mini-bar + "N of X slots available"
   text. Retires the green-dot pill for COUNTERS (the category ✓/🔒 chips
   are the separate, locked category display and keep the pill). */
.pf-counter { display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-small); font-weight: 700; line-height: 1.2; }
.pf-counter .dk-segs { margin-right: 0; flex: none; }
.pf-counter--low .pfc-t { color: #C22C00; }
.pf-counter.skel { min-width: 150px; min-height: 16px; border-radius: 4px; }

/* (V1 area grid rules removed with the relic area cards — orphan sweep 2026-08-05) */

/* ---- Numbered configurator ---- */
.cfg-step { padding-block: var(--s-2); border-bottom: 1px solid var(--pf-border); }
.cfg-step:last-of-type { border-bottom: 0; }
.cfg-step .cfg-label {
  display: flex; align-items: center; gap: var(--s-1);
  font-weight: 700; font-size: 15px; margin-bottom: var(--s-1);
}
/* the SPEC step chips were RETIRED (Jon, 2026-08-09): the three
   labelled areas and the guide bar carry the sequence — a numbered
   ladder inside SPEC duplicated it. Rules removed with the markup. */
.price-lines { display: grid; gap: 6px; margin-top: var(--s-2); }
.price-lines .pl { display: flex; justify-content: space-between; gap: var(--s-2); font-size: 15px; color: var(--pf-grey-600); min-height: 24px; padding-inline: var(--s-1); }
.price-lines .pl.total { border-top: 1px solid var(--pf-border); padding-top: var(--s-1); font-weight: 800; font-size: 19px; color: var(--pf-black); }
.price-lines .pl .val { font-weight: 700; color: var(--pf-black); }
.price-lines .pl.save .val { color: var(--pf-green); }

/* ---- Toggle switch ---- */
.switch { position: relative; display: inline-flex; align-items: center; gap: var(--s-1); cursor: pointer; min-height: 44px; }
.switch input { position: absolute; opacity: 0; width: 44px; height: 24px; }
.switch .track {
  width: 44px; height: 24px; border-radius: var(--r-pill); background: var(--pf-border);
  transition: background-color var(--t-fast) var(--ease); flex: 0 0 auto; position: relative;
}
.switch .track::after {
  content: ""; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px;
  border-radius: 50%; background: var(--pf-white); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
  transition: transform var(--t-fast) var(--ease);
}
.switch input:checked + .track { background: var(--pf-orange); }
.switch input:checked + .track::after { transform: translateX(20px); }
.switch input:focus-visible + .track { outline: 2px solid var(--pf-orange); outline-offset: 2px; }
.switch .sw-label { font-weight: 600; font-size: 15px; }

/* ---- Crossfade content swap (never a hard swap) ---- */
.xfade { animation: pf-xfade var(--t-fast) var(--ease); }
@keyframes pf-xfade { from { opacity: 0; transform: translateY(8px); } }

/* ---- Stagger on card grids ---- */
.js .stagger.reveal.in > * { animation: pf-rise 0.5s var(--ease); }
.js .stagger.reveal.in > :nth-child(2) { animation-delay: 70ms; }
.js .stagger.reveal.in > :nth-child(3) { animation-delay: 140ms; }
.js .stagger.reveal.in > :nth-child(4) { animation-delay: 210ms; }
.js .stagger.reveal.in > :nth-child(5) { animation-delay: 280ms; }
.js .stagger.reveal.in > :nth-child(n+6) { animation-delay: 350ms; }

/* ---- Tick lists / step visuals ---- */
.ticks { list-style: none; display: grid; gap: var(--s-1); }
.ticks li { display: flex; gap: var(--s-1); align-items: center; font-weight: 600; font-size: 15px; }
.ticks li::before { content: "✓"; color: var(--pf-green); font-weight: 800; }
.step-visual { border-radius: var(--s-1); overflow: hidden; border: 1px solid var(--pf-border); margin-bottom: var(--s-2); aspect-ratio: 3 / 2; background: var(--pf-grey-50); display: flex; align-items: center; justify-content: center; }
.step-visual img, .step-visual svg { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ---- Testimonials: component exists, HIDDEN until real reviews collected ---- */
.testimonials[hidden] { display: none !important; }

/* ==========================================================================
   FOLD LAYER — compact nav, product-toggle spine, one-viewport hero.
   Later rules override the original header/hero/system where they differ.
   ========================================================================== */

/* Density: section rhythm cut ~35% (fold mandate v2) */
:root { --section-y: 40px; }
@media (min-width: 768px) { :root { --section-y: var(--s-8); } }
@media (min-width: 1140px) { :root { --section-y: 64px; } }

/* Compact nav: 56px, logo 130, quiet 14px centred links, compact CTA */
.nav-row { min-height: 56px; padding-block: 0; gap: var(--s-2); flex-wrap: nowrap; }
.brand { min-height: 44px; padding-block: 6px; }
.brand img { width: 146px; height: auto; } /* +12%; 26px tall — 56px nav unchanged */
.site-nav { margin-inline: auto; }
.site-nav ul { gap: var(--s-3); }
.site-nav ul a { font-size: 14px; font-weight: 500; padding: 15px 0; }
.nav-cta.btn-small { padding: 9px var(--s-2); font-size: 14px; font-weight: 700; line-height: 1.2; }
.nav-toggle { display: none; }
.nav-burger { display: none; }
@media (max-width: 767px) {
  .nav-row { flex-wrap: nowrap; }
  .nav-burger {
    display: flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; cursor: pointer; border-radius: var(--s-1);
    order: 3; flex: 0 0 auto;
  }
  .nav-burger:hover { background: var(--pf-grey-50); }
  .nav-burger svg { pointer-events: none; }
  .nav-cta { order: 2; margin-left: auto; flex: 0 0 auto; }
  .site-nav { order: 4; flex: 1 1 100%; display: none; margin: 0; }
  .nav-toggle:checked ~ .site-nav { display: flex; padding-bottom: var(--s-1); }
  .nav-row:has(.nav-toggle:checked) { flex-wrap: wrap; }
}

/* Product-toggle spine — the page's primary product navigation */
.spine { border-bottom: 1px solid var(--pf-border); background: var(--pf-white); }
.spine-inner { display: flex; flex-direction: column; align-items: center; gap: var(--s-1); padding-block: var(--s-1); }
.spine-pills {
  display: inline-flex; background: var(--pf-grey-50); border: 1px solid var(--pf-border);
  border-radius: var(--r-btn); padding: 4px; gap: 4px; /* one-corner law: pills left the vocabulary */
}
.spine-pills a, .spine-pills button {
  font: 700 15px/1 Inter, system-ui, sans-serif; color: var(--pf-grey-600);
  background: transparent; border: 0; border-radius: 8px; /* inner rows sit 2px inside the 10px token frame */
  padding: 12px 22px; cursor: pointer; text-decoration: none; white-space: nowrap;
  transition: background-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.spine-pills [aria-pressed="true"], .spine-pills [aria-current="page"] {
  background: var(--pf-white); color: var(--pf-black); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
.spine-pills [aria-pressed="true"] { color: var(--pf-orange); }
.spine-pills [aria-current="page"] { color: var(--pf-orange); }
.spine-sub { font-size: var(--fs-small); line-height: var(--lh-small); font-weight: 600; color: var(--pf-grey-600); text-align: center; }
@media (max-width: 480px) { .spine-sub { font-size: 13px; } }

/* Fold hero: fills the remaining viewport under nav+spine (no taller) */
.fold { display: grid; }
.fold-view { grid-area: 1 / 1; visibility: hidden; }
.fold-view.active { visibility: visible; }
.js .fold-view.active.xfade { animation: pf-xfade 200ms var(--ease); }
.fold .hero { padding-block: var(--s-4); }
.fold .hero h1 { font-size: clamp(36px, 2.6vw + 22px, 52px); }
.fold .hero p.lead { margin-top: var(--s-2); }
.fold .price-chip { margin-top: var(--s-2); }
.fold .hero-ctas { margin-top: var(--s-2); }
.fold .fold-scarcity { margin-top: var(--s-2); }
@media (min-width: 900px) {
  /* Top-align: the tall card bleeds past the fold (invites scroll); the pitch
     column (H1 → scarcity) must sit fully inside it. */
  .fold .hero-grid { align-items: start; }
  .fold .hero-grid > div:first-child { padding-top: var(--s-6); }
}
.price-chip {
  /* inline-block, NOT flex: flex discards the ordinary spaces and renders the
     8px gap instead — the long-lived "double-width gaps around £199" bug */
  display: inline-block;
  background: var(--pf-orange-tint); color: var(--pf-orange);
  border-radius: var(--r-btn); padding: 10px 18px; /* a box, not a pill (one-corner law) */
  font-size: 19px; font-weight: 800; margin-top: var(--s-2);
}
.fold-scarcity { margin-top: var(--s-2); min-height: 24px; font-size: 15px; font-weight: 600; color: var(--pf-grey-600); }
.fold .hero p.lead { margin-top: var(--s-2); }
.fold .hero-ctas { margin-top: var(--s-3); }
/* the composed card is the dominant object: 60%+ of its column */
.fold .composed-card { max-width: none; }
.fold .fold-solo-art { max-width: 420px; margin-inline: auto; border-radius: 0; /* paper ruling */ overflow: hidden; box-shadow: var(--shadow-print); border: 1px solid var(--pf-border); }
.fold .fold-solo-art img { display: block; width: 100%; height: auto; }
@media (max-width: 899px) {
  .fold .hero { padding-block: var(--s-3); }
  .fold .hero-grid { gap: var(--s-3); }
}

/* ==========================================================================
   DECK LAYER — persistent booking interface (top of page IS the product)
   ========================================================================== */
:root { --container: 1360px; }
@media (min-width: 1024px) { :root { --container: min(100% - 64px, 1760px); } } /* LAYOUT-STAGE-02: genuinely wide */
@media (min-width: 768px) { :root { --gutter: var(--s-4); } }

/* Deck tabs: quiet underline style; sparkle marks the AI designer */
.deck { border-bottom: 1px solid var(--pf-border); background: var(--band, #FAF7F4); } /* banded: gutters read designed */
.deck-tabs { display: flex; gap: var(--s-4); border-bottom: 1px solid var(--pf-border); overflow-x: auto; scrollbar-width: none; }
.deck-tabs::-webkit-scrollbar { display: none; }
.deck-tabs button {
  font: 700 16px/1 Inter, system-ui, sans-serif; color: var(--pf-grey-600);
  background: none; border: 0; border-bottom: 3px solid transparent;
  padding: var(--s-2) 2px 14px; cursor: pointer; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 6px;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.deck-tabs button:hover { color: var(--pf-black); }
.deck-tabs button[aria-selected="true"] { color: var(--pf-black); border-bottom-color: var(--pf-orange); }
.deck-tabs .sparkle { color: var(--pf-orange); }

/* ── THE THREE-SECTION LAYOUT LAW (Jon-ruled 2026-08-09) ──────────────
   SPEC | DESIGN | CONFIRM — three separate, LABELLED, visibly distinct
   areas. Desktop: three across. Mobile: the same three, same labels,
   same order, met on scroll. The customer always knows which of the
   three they are in. Supersedes the two-column body + nested split. */
.deck-body { display: grid; gap: var(--s-3); padding-block: var(--s-3); grid-template-columns: 1fr; }
/* THE TAB AND THE BOX ARE ONE SHAPE (Jon's mobile walk, 2026-08-09): the
   filled tab sat inside the box's 1px border still drawing the box's OWN
   10px radius, so at each top corner the two arcs ran a pixel apart and
   the box's white ground showed through as a sliver of colour. The inner
   radius of a bordered box is the outer radius MINUS the border width —
   both are tokens here so the two can never drift again. */
.zone {
  --zone-r: 10px; --zone-bw: 1px;
  min-width: 0; border: var(--zone-bw) solid var(--pf-border); border-radius: var(--zone-r);
  background: var(--pf-white); padding: 0 var(--s-2) var(--s-2);
}
/* ── WHERE THE SECTIONS FOLD, THE HEADER IS A CONTROL AND SAYS SO (Jon-ruled
   2026-08-24: "a control that does not announce itself is not one") ─────────
   Below 1200 only the live section shows its body, so pressing a header is how
   a customer opens one: it gets role/tabindex/aria-expanded from the same
   writer that folds it, plus the cursor and the focus ring here.
   ABOVE 1200 it is not a control and does not pretend to be one: all three
   columns stand open, and the live section follows wherever the customer last
   touched — the header included, but so is every other thing in the column.
   That measurement is why this is scoped rather than global. */
@media (max-width: 1199px) {
  .zone-lab[role="button"] { cursor: pointer; }
  .zone-lab[role="button"]:focus-visible {
    outline: 3px solid var(--pf-orange); outline-offset: -3px;
  }
}
.zone-lab {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin: 0 calc(var(--s-2) * -1) var(--s-2); padding: 10px var(--s-2);
  border-bottom: 1px solid var(--pf-border); background: var(--pf-ink, #111);
  border-radius: calc(var(--zone-r, 10px) - var(--zone-bw, 1px)) calc(var(--zone-r, 10px) - var(--zone-bw, 1px)) 0 0;
  font: 800 13px/1.2 Inter, system-ui, sans-serif; letter-spacing: .08em; text-transform: uppercase;
}
.zone-lab .zone-n {
  flex: none; width: 20px; height: 20px; border-radius: 50%;
  background: var(--pf-ink, #111); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700; letter-spacing: 0; align-self: center;
}
.zone-lab small { font: 400 12px/1.3 Inter, system-ui, sans-serif; letter-spacing: 0; text-transform: none; color: rgba(255, 255, 255, .88); }
/* ── THE LIVE COUNT, WHERE A HANDSET CUSTOMER CAN READ IT (Jon-ruled
   2026-09-04) ─────────────────────────────────────────────────────────────
   The tile counter is retired (27 Aug: at half scale it rendered 4.5px tall
   and read as a stray image, not as words), so on a phone the count is stated
   as TEXT in the CONFIRM header instead. 12px, the same treatment the header's
   own small print already uses, on the dark bar: legible at 390 without
   shouting. It is NOT shown from 1200 up, where the stage sheet carries its
   own masthead counter at full size and a second copy would be two surfaces
   making the same claim. Ships hidden and empty, and only the sheet's own
   data fills it. */
.zone-count { display: none; }
@media (max-width: 1199px) {
  .zone-count:not([hidden]) {
    display: inline; margin-left: auto;
    font: 400 12px/1.3 Inter, system-ui, sans-serif; letter-spacing: 0; text-transform: none;
    color: rgba(255, 255, 255, .88);
  }
}
/* ── THE GUIDANCE LAW (Jon-ruled 2026-08-09) ──────────────────────────
   The bar says which step the customer is on; the PAGE says the same
   thing in the same breath. EXACTLY ONE area carries the highlight at
   any moment — guaranteed by construction, because the state is a single
   attribute value written by the bar's own render (its only writer).
   THE REGISTER IS THE BRAND ORANGE (Jon's mobile walk, 2026-08-09 — this
   SUPERSEDES the --pf-attention register here): --pf-attention (#CC2A01)
   is the brand red darkened for TEXT on white, and as a large FILL it
   read plainly RED on the phone. The chrome is a fill and a boundary, not
   a line of text, so it takes the ratified brand orange itself —
   var(--pf-orange) = #FD3601. As a non-text boundary it measures 3.68:1
   on white and 3.45:1 on cream, both past the 3:1 a boundary needs.
   --pf-attention keeps its own job (guidance TEXT on white), unchanged. */
html[data-pf-step="0"] .zone[data-zone="spec"],
html[data-pf-step="1"] .zone[data-zone="design"],
html[data-pf-step="2"] .zone[data-zone="confirm"] {
  border-color: var(--pf-orange);
  box-shadow: 0 0 0 2px var(--pf-orange);
}
html[data-pf-step="0"] .zone[data-zone="spec"] > .zone-lab,
html[data-pf-step="1"] .zone[data-zone="design"] > .zone-lab,
html[data-pf-step="2"] .zone[data-zone="confirm"] > .zone-lab {
  border-bottom-color: var(--pf-orange);
}
/* COMPLETION CHROME (guidance law v2, AMENDED by Jon 2026-08-09): a
   finished area wears a FOREST-GREEN box with the tick top-right — the
   flyer's own register, which pairs with the attention orange and suits
   the brand. "This is done, move on." Click back into it and the live
   rules above repaint it orange; click away and it returns to green +
   tick while guidance moves to the next unmet step. Never both: the live
   area is never in the done list (one reading writes both).
   CONTRAST, MEASURED NOT EYEBALLED: the green reads 5.41:1 on white and
   5.07:1 on the cream ground — past the 4.5:1 TEXT needs, not merely the
   3:1 a boundary needs — and white text on it reads the same 5.41:1. The walk
   re-measures both from the PAINTED pixels every run.
   NOT COLOUR ALONE: green and the attention orange sit only 2.48:1 apart
   in luminance, so they are told apart by HUE — which some customers
   cannot rely on. The TICK GLYPH is therefore the real "done" signal and
   the orange state carries none: the state survives without colour. */
/* (the completion chrome uses --pf-green: ONE token, no second name) */
/* ── COMPLETION IS BLACK + TICK (Jon-ruled 2026-08-10; SUPERSEDES the
   forest-green completion chrome of v66–v68) ──────────────────────────
   Green leaves the state chrome entirely. The language is now three
   plain states and one of them is the resting state:
     black, no tick .... not reached yet
     ORANGE ............ live, your turn
     black + TICK ...... done
   So a done section does not change colour on completion — it gains a
   TICK. The glyph carries the meaning outright, which is the strongest
   form of the not-colour-alone rule this chrome has had. */
html[data-pf-done~="spec"] .zone[data-zone="spec"],
html[data-pf-done~="design"] .zone[data-zone="design"],
html[data-pf-done~="confirm"] .zone[data-zone="confirm"] {
  border-color: var(--pf-ink, #111);
  box-shadow: 0 0 0 2px var(--pf-ink, #111);
}
html[data-pf-done~="spec"] .zone[data-zone="spec"] > .zone-lab,
html[data-pf-done~="design"] .zone[data-zone="design"] > .zone-lab,
html[data-pf-done~="confirm"] .zone[data-zone="confirm"] > .zone-lab {
  border-bottom-color: var(--pf-ink, #111);
}
/* ── LIVE-SECTION EMPHASIS (Jon-ruled 2026-08-09) ─────────────────────
   The LIVE section's own content steps darker and clearer, so the whole
   working area draws the eye — not just its border. Resting sections
   (waiting or complete) sit quieter by comparison.
   ONE SOURCE OF TRUTH: this keys off the SAME data-pf-step the highlight,
   the tab fill and the bar's ticks read. No classes, no third state.
   INSIDE THE LEGIBILITY TOKEN SET: live text steps to --pf-black (#1A1A1A)
   and live labels to it too; resting body text rests at --pf-grey-600
   (#6B6B6B), which is the ratified quiet register. Nothing invents a new
   grey, and nothing drops below AA:
     live label/text  #1A1A1A on white .... 17.40:1  (cream 16.34:1)
     resting text     #6B6B6B on white ....  5.33:1  (cream  5.00:1)
   IT SUPPORTS THE ORANGE, IT DOES NOT COMPETE: the emphasis is WEIGHT and
   INK only — no colour of its own — so the orange stays the single
   coloured signal in the area. */
.zone[data-zone] .cfg-label, .zone[data-zone] label, .zone[data-zone] .field label {
  color: var(--pf-grey-600);
}
html[data-pf-step="0"] .zone[data-zone="spec"] .cfg-label,
html[data-pf-step="0"] .zone[data-zone="spec"] label,
html[data-pf-step="1"] .zone[data-zone="design"] .cfg-label,
html[data-pf-step="1"] .zone[data-zone="design"] label,
html[data-pf-step="2"] .zone[data-zone="confirm"] .cfg-label,
html[data-pf-step="2"] .zone[data-zone="confirm"] label {
  color: var(--pf-black);
}
/* the live area's inputs read crisper: a firmer edge, white ground, and
   the customer's own typing at full ink */
html[data-pf-step="0"] .zone[data-zone="spec"] :is(input:not([type="range"]), select, textarea),
html[data-pf-step="1"] .zone[data-zone="design"] :is(input:not([type="range"]), select, textarea),
html[data-pf-step="2"] .zone[data-zone="confirm"] :is(input:not([type="range"]), select, textarea) {
  /* TEXT FIELDS ONLY (scope named 2026-09-03): a range input has no ground of
     its own to whiten — this rule painted #fff over the Precision slider's
     track and fill, leaving the handles floating on nothing. */
  border-color: #B9B2AA; color: var(--pf-black); background: #fff;
}
/* resting sections recede — quieter ink, never unreadable (5.33:1) */
html[data-pf-step="1"] .zone[data-zone="spec"] :is(input:not([type="range"]), select, textarea),
html[data-pf-step="2"] .zone[data-zone="spec"] :is(input:not([type="range"]), select, textarea),
html[data-pf-step="0"] .zone[data-zone="design"] :is(input:not([type="range"]), select, textarea),
html[data-pf-step="2"] .zone[data-zone="design"] :is(input:not([type="range"]), select, textarea) {
  color: var(--pf-grey-600);
}

/* ── THE SECTION HEADER TABS (Jon-ruled 2026-08-09) ───────────────────
   The strip carrying the number and title FILLS with its state: orange
   while it is the customer's turn, green once the section is complete —
   matching its own outline. Inside the filled tab: white text, and a
   WHITE CIRCLE carrying the number (in orange, while active) or the tick
   (in green, when complete).
   The fill derives from the SAME live reading as the box highlight and
   the bar's ticks — one writer, one truth — so tab, box and bar cannot
   disagree. Re-enter a completed section and its tab flips back to orange
   with its number; click away and it returns to green with its tick. */
/* ── THE HEADER TAB SPEC (Jon-ruled 2026-08-09; supersedes the ink-on-
   orange reading I shipped an hour earlier) ───────────────────────────
   ONE RULE, BOTH STATES, ALL THREE SECTIONS: on the solid state fill the
   text is WHITE and the circle is WHITE, and the glyph inside the circle
   takes THE SAME COLOUR AS THE HEADER BACKGROUND — an orange number while
   the section is live, a green tick once it is complete.
   MEASURED, AND ONE NUMBER IS NAMED HONESTLY (gate-mobile-composition.js
   re-measures both off the painted pixels every run):
     white on the forest  #175F3F .... 7.65:1  — passes AA at any size
     white on brand orange #FD3601 ... 3.68:1  — passes the 3:1 that
        non-text and large text need; it is BELOW the 4.5:1 that text
        under 18.66px bold needs. The alternative is the darkened brand
        red, which is the thing Jon's walk rejected as "red". The ruled
        design stands and the shortfall is NAMED in the gate and in
        DECISIONS-PENDING rather than quietly accepted — his call. */
html[data-pf-step="0"] .zone[data-zone="spec"] > .zone-lab,
html[data-pf-step="1"] .zone[data-zone="design"] > .zone-lab,
html[data-pf-step="2"] .zone[data-zone="confirm"] > .zone-lab {
  /* the live fill carries white text, so it takes the text-bearing orange:
     5.04:1 in place of 3.68:1, and it still reads as the same colour */
  background: var(--pf-orange-deep); color: #fff;
}
/* THE RESTING TAB IS BLACK, for every section, reached or not — the tick
   is what completion adds. White text and a white circle on both fills;
   the glyph inside the circle takes the tab's OWN colour, so it reads
   black on a resting tab and orange on the live one.
   MEASURED: white on #1A1A1A = 17.40:1 · white on brand orange = 3.68:1
   (the named shortfall, Jon's ruled design — gate prints it every run). */
.zone[data-zone] > .zone-lab { background: var(--pf-ink, #111); color: #fff; }
.zone[data-zone] > .zone-lab small { color: rgba(255, 255, 255, .88); }
.zone[data-zone] .zone-n { background: #fff; color: var(--pf-ink, #111); }
html[data-pf-done~="spec"] .zone[data-zone="spec"] > .zone-lab,
html[data-pf-done~="design"] .zone[data-zone="design"] > .zone-lab,
html[data-pf-done~="confirm"] .zone[data-zone="confirm"] > .zone-lab {
  background: var(--pf-ink, #111); color: #fff;
}
/* the subtitle is white too — at FULL white, not the old 88%, because on
   the orange every point of contrast counts */
html[data-pf-step="0"] .zone[data-zone="spec"] > .zone-lab small,
html[data-pf-step="1"] .zone[data-zone="design"] > .zone-lab small,
html[data-pf-step="2"] .zone[data-zone="confirm"] > .zone-lab small,
html[data-pf-done~="spec"] .zone[data-zone="spec"] > .zone-lab small,
html[data-pf-done~="design"] .zone[data-zone="design"] > .zone-lab small,
html[data-pf-done~="confirm"] .zone[data-zone="confirm"] > .zone-lab small {
  color: #fff;
}
/* the white circle, its glyph in the header's own colour */
html[data-pf-step="0"] .zone[data-zone="spec"] .zone-n,
html[data-pf-step="1"] .zone[data-zone="design"] .zone-n,
html[data-pf-step="2"] .zone[data-zone="confirm"] .zone-n {
  /* the glyph takes the tab's OWN colour (his tab spec), so when the live
     fill moved to the text-bearing orange this had to move with it — the
     gate caught the pair drifting apart the moment they did */
  background: #fff; color: var(--pf-orange-deep);
}
html[data-pf-done~="spec"] .zone[data-zone="spec"] .zone-n,
html[data-pf-done~="design"] .zone[data-zone="design"] .zone-n,
html[data-pf-done~="confirm"] .zone[data-zone="confirm"] .zone-n {
  background: #fff; color: var(--pf-ink, #111); font-size: 0;
}
html[data-pf-done~="spec"] .zone[data-zone="spec"] .zone-n::after,
html[data-pf-done~="design"] .zone[data-zone="design"] .zone-n::after,
html[data-pf-done~="confirm"] .zone[data-zone="confirm"] .zone-n::after {
  content: "✓"; font-size: 13px; font-weight: 800; line-height: 1;
}
.zone { position: relative; }
/* (the corner tick RETIRED with the tab ruling — the tick lives in the
   tab's white circle now; two ticks would be two voices for one state) */
/* the bar's own tick — the same completion, said in the same breath */
.pfg-tick { color: inherit; font-weight: 800; }
.pfg-did { opacity: 1; }

@media (prefers-reduced-motion: no-preference) {
  .zone { transition: box-shadow 200ms var(--ease, ease), border-color 200ms var(--ease, ease); }
}
.zone[data-zone="design"] { background: var(--pf-cream, #FBF7F4); }
/* (RETIRED 2026-08-10, Jon: the DESIGN tab's white ground was left over
   from the cream design zone, and with white tab text it made the heading
   disappear. ONE RULE FOR ALL THREE TABS, no exemptions — black at rest
   and when complete, orange when live.) */
/* ── CONFIRM CARRIES THE ORDER BELOW 1200 TOO (Jon's mobile walk) ─────
   The pricing pieces dock into the rail at every width now, and below
   1200 the rail has no card treatment of its own — so give the block the
   same air it has on the desktop rail, and let the booking button take
   the full width the phone offers. Nothing here is a second panel: these
   are the SAME nodes, in the one place the law puts them. */
@media (max-width: 1199px) {
  .deck-rail .pricing-slot { margin-top: var(--s-2); display: grid; gap: 4px; }
  .deck-rail .pricing-slot .rail-price { display: flex; flex-direction: column; gap: 6px; }
  .deck-rail .pricing-slot .rail-price[hidden] { display: none; }
  .deck-rail .pricing-slot .price-lines { margin-top: 0; }
  .deck-rail .pricing-slot > .rail-price > .btn { width: 100%; margin-top: 4px; }
}
@media (min-width: 980px) { .deck-body { grid-template-columns: minmax(340px, 400px) minmax(0, 1fr); align-items: start; }
  .zone[data-zone="confirm"] { grid-column: 1 / -1; } }
@media (min-width: 1200px) { .deck-body { grid-template-columns: 360px minmax(0, 1fr) 380px; }
  .zone[data-zone="confirm"] { grid-column: auto; } }
/* ── THE DESIGN WINDOW (Jon-ruled 2026-08-09, extends the three-section
   law) ── The middle EXPANDS OUT carrying all its controls. It is the
   SAME PAGE: no modal blackout, no overlay swallowing the page, no route
   change — SPEC stays visible beside it (underneath on the phone, where
   there is no beside). The X restores the three areas exactly as they
   were. Nothing MOVES in the DOM, so every input, the design and the
   spec persist in both directions BY CONSTRUCTION. */
/* the artefact takes the room the window just gave it — SIZED, not
   unleashed: a height the expanded canvas can hold, so the whole face is
   visible and nothing needs scrolling */
@media (max-width: 1199px) {  }
@media (max-width: 979px) {
  /* ON THE PHONE IT MUST GENUINELY BECOME THE WORKING VIEW (Jon-ruled
     2026-08-10; the in-flow version was "a tiny width increase, useless").
     Same law as the desktop: centred, filling the space, never
     overflowing — which on a phone means the screen below the sticky
     chrome. Still the same page: no backdrop, no dialog, no route
     change, and the X puts everything back exactly. */
      }
/* the artefact takes the room the window gives it (host-box contract) */

/* THE MIDDLE OWNS EVERYTHING DESIGN: the look gallery and the migrated
   artwork controls sit BELOW the mockup, uniform treatment throughout */
.dz-under { margin-top: var(--s-2); display: grid; gap: var(--s-2); }
.dz-under .dz-under-block[hidden] { display: none; }
.dz-under .seg { width: 100%; }
/* THE ZOOM, RE-RULED (three-section law): the earlier zoom widened the
   middle by COLLAPSING THE GRID — on the designer tab that dropped the
   CONFIRM area off the row entirely, and the customer lost one of the
   three. Superseded: the three areas hold their places at every tab, and
   the zoom is the ARTEFACT's own (zoom in to work, out to see whole). */
@media (min-width: 1024px) and (max-width: 1199px) {
  .deck[data-deck="designer"] .deck-body { grid-template-columns: 320px minmax(0, 1fr); }
}
/* THE EMPTY CANVAS DOES NOT OWN THE SCREEN: before a design lands, the
   stage stays modest so the LOOK GALLERY below it is in view on entry
   (the picker fronts every designer entry — it must be SEEN to front). */
.deck[data-deck="designer"] .artefact-stage { max-height: 58vh; }
.deck[data-deck="designer"] .artefact-stage > [data-pv="designer"] { max-height: 100%; }
.deck[data-deck="designer"] #pf-designer-preview { max-height: 54vh; width: auto; max-width: 100%; }
body[data-pf-design="landed"] .deck[data-deck="designer"] .artefact-stage { max-height: 76vh; }
body[data-pf-design="landed"] .deck[data-deck="designer"] #pf-designer-preview { max-height: 72vh; }
.deck-panes { display: grid; }
.deck-pane { grid-area: 1 / 1; display: none; } /* no reserved height from inactive panes */
.deck-pane.active { display: block; }
.js .deck-pane.active.xfade { animation: pf-xfade 200ms var(--ease); }
/* THE 11px OF STUBS (Jon-ruled 2026-08-24: "drop the 11px of stubs, LEAVE the
   breathing room"). `.deck-preview` measured 6px inside the design section
   with every child either hidden or out of flow — a line box made by the
   whitespace between its tags, nothing more; its children set their own line
   height. `#dz-steps` measured 5px as an empty grid whose items are all
   hidden. Neither is content, and together they are the difference between a
   35px step at 1200 and a 46px one. */
.deck-preview { display: block; line-height: 0; } /* LAYOUT-STAGE-01: the stage handles crossfade; slots are plain flow */
.deck-preview > * { line-height: normal; }
#dz-steps:not(:has(> :not([hidden]))) { display: none; }

.deck .cfg-step { padding-block: 12px; }
.deck .price-chip { margin-top: var(--s-1); }
.per-letterbox { font-size: var(--fs-small); font-weight: 700; color: var(--pf-green); min-height: var(--lh-small); }

/* Coming-soon rows inside booking controls — the form sells the roadmap */
.addon-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-2);
  padding: 9px 0; border-top: 1px dashed var(--pf-border);
  color: var(--pf-grey-400); font-size: 15px; font-weight: 600; cursor: pointer;
}
.addon-row:hover .chip { background: rgba(253, 54, 1, 0.12); }
.addon-row .chip { padding: 5px 10px; font-size: 11px; color: var(--pf-orange); background: var(--pf-orange-tint); }
.addon-row .chip::before { display: none; }

/* Waitlist modal */
.wl-modal { border: 1px solid var(--pf-border); border-radius: var(--r-card); padding: var(--s-4); max-width: 420px; width: calc(100vw - 48px); }
.wl-modal::backdrop { background: rgba(26, 26, 26, 0.45); }
.wl-modal h3 { margin-bottom: var(--s-1); }
.wl-modal .close { float: right; }

/* Landscape composed card + 3D side flip (display fix: the card IS A5 landscape) */
.cc3d { perspective: 1600px; }
.cc3d-inner { position: relative; transform-style: preserve-3d; transition: transform 400ms var(--ease); }
.cc3d.flipped .cc3d-inner { transform: rotateY(180deg); }
.cc3d .cc-face { backface-visibility: hidden; }
.cc3d .cc-face.back { position: absolute; inset: 0; transform: rotateY(180deg); }
/* DETAIL-07 flip return: backface-visibility hides PAINT only — the turned-
   away face still hit-tests, and the invisible Side A sat over Side B's
   "Flip for Side A" control, eating every real tap. The away face is inert. */
.cc3d:not(.flipped) .cc-face.back { pointer-events: none; }
.cc3d.flipped .cc-face.front { pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .cc3d-inner { transition: none; } }
/* Single flip control: the status bar's right-hand action */
.cc-foot-flip {
  display: inline-flex; align-items: center; gap: 6px;
  font: 700 12px/1 Inter, system-ui, sans-serif; color: var(--pf-orange);
  background: none; border: 0; padding: 6px 0; cursor: pointer;
  transition: color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.cc-foot-flip:hover { color: var(--pf-orange-dark); transform: translateY(-1px); }
.cc-foot-flip svg { flex: 0 0 auto; }

/* CARD-PORTRAIT-01: the portrait card sizes by HEIGHT inside the fixed
   stage box (geometry reserved; width follows the A5 ratio) */
/* FACE-CALM-01: ONE FIXED CARD SIZE PER TIER. The old height-basis rule
   (height:100%, width from the rail via aspect) let the RAIL height set
   the card width — the card overflowed or underfilled its fixed column and
   varied with rail content. WIDTH-basis: the fixed column IS the card
   size; the stage centres it; the rail stretches independently. */
.deck-preview [data-composed-card] { margin-inline: auto; width: 100%; height: auto; min-height: 0; display: block; }
.deck-preview [data-composed-card] .cc3d { width: 100%; height: auto; } .deck-preview [data-composed-card] .cc3d-inner { height: 100%; }
/* MOBILE-CARD-PREVIEW-01 s1/s2 root cause: the stage system sizes the card
   by HEIGHT (the height:100% chain above + the face's aspect-ratio deriving
   width FROM height). A solo <img> self-sizes from its intrinsic ratio, so
   it never needed a container — the DOM card does, and outside a properly
   height-sized stage (the sheet, the hero tile) it collapsed to a sliver
   while only its wrapper scaled. In clone/sheet contexts the basis is
   pinned to WIDTH at the 460px metric floor; the wrapper then scales as
   one pixel-faithful object. */
.sheet-body .artefact-stage [data-pv="card"] [data-composed-card],
.peek-scale [data-composed-card] { height: auto; }
.sheet-body .artefact-stage [data-pv="card"] [data-composed-card] .cc3d,
.peek-scale [data-composed-card] .cc3d { width: 100%; height: auto; aspect-ratio: 148 / 210; }
.sheet-body .artefact-stage [data-pv="card"] [data-composed-card] .cc3d-inner,
.peek-scale [data-composed-card] .cc3d-inner { height: 100%; }
.sheet-body .artefact-stage > [data-pv="card"] { overflow: visible; }
.deck-preview [data-composed-card] .composed-card { height: 100%; width: auto; }
/* homepage/fold marketing card: capped width, portrait */
.fold [data-composed-card] { max-width: 400px; margin-inline: auto; }

/* Amber = degraded state (green is reserved for live data) */
.chip--warn { color: #8A5A00; background: #FFF4D6; }

/* Price block rhythm (desktop): consistent 8px gaps, step-content alignment */
.price-block { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-1); margin-top: var(--s-2); }
.price-block .scarcity-line { display: none; } /* rail card (>=1600) only */
.price-block .price-chip, .price-block .per-letterbox, .price-block .fold-scarcity { margin: 0; }

/* Consumer masthead: monthly offers first, PromoFlyers second */
.cc-mast {
  padding: var(--s-2) var(--s-3); background: var(--pf-black); color: var(--pf-white);
  min-height: 12.4%; display: flex; flex-direction: column; justify-content: center;
}
.cc-mast .m-quiet { font-size: clamp(17px, 1.8vw, 24px); }
/* CARD-FACE-01: face branding is MONO - gold retired with the old mast */
.composed-card .cc-grid--face { grid-template-columns: 1fr 1fr; }
.deck .fold-solo-art { max-width: 360px; }
.deck .designer-preview { max-width: 640px; }
.upload-note { font-size: var(--fs-small); color: var(--pf-grey-600); margin-top: var(--s-1); min-height: var(--lh-small); }

/* ==========================================================================
   MOBILE DECK LAYER (≤768px only — desktop layout untouched)
   Peek strip · bottom-sheet preview · controls compression · sticky CTA
   ========================================================================== */
.only-mobile { display: none; }
/* DETAIL-07: ONE house info icon — inline SVG (stroke 1.6, the close/expand
   chip family), border-grey default, brand-orange on hover/focus/open. The
   old text glyph rendered grainy and off-system. Touch gets a 44px hit area
   via ::after — zero layout impact, immune to per-context padding rules. */
.info-i { display: none; background: none; border: 0; color: var(--pf-border); line-height: 0; vertical-align: middle; cursor: pointer; padding: 4px 8px; position: relative; transition: color .15s ease-out; }
.info-i svg { display: inline-block; vertical-align: middle; }
.info-i:hover, .info-i:focus-visible, .info-i[aria-expanded="true"] { color: var(--pf-brand); }
@media (hover: none) {
  .info-i::after { content: ""; position: absolute; left: 50%; top: 50%; width: 44px; height: 44px; transform: translate(-50%, -50%); }
}
.info-pop {
  position: absolute; z-index: 30; background: var(--pf-black); color: var(--pf-white);
  font-size: 13px; line-height: 1.4; padding: 8px 12px; border-radius: 8px; max-width: 240px;
}
@keyframes pf-pulse { 0% { transform: scale(1); } 50% { transform: scale(1.02); } 100% { transform: scale(1); } }
.peek-strip { display: none; }

/* Bottom sheet (dialog top layer sits above the sticky CTA by definition) */
.sheet {
  position: fixed; inset: auto 0 0 0; margin: 0; width: 100%; max-width: none;
  border: 0; border-radius: var(--r-card) var(--r-card) 0 0; padding: var(--s-2) var(--s-3) var(--s-4);
  max-height: 86dvh; overflow: hidden; /* the BODY scrolls; the frame (and X) stay put */
  display: flex; flex-direction: column;
}
.sheet:not([open]) { display: none; } /* author display beats the UA closed-dialog rule */
.sheet-body { overflow-y: auto; flex: 1; min-height: 0; }
.sheet, .sheet-body { max-width: 100vw; }
.sheet-body img, .sheet-body .leaflet-container { max-width: 100%; }
.sheet-body { overflow-x: hidden; } /* content may scroll vertically, never off-screen sideways */
.sheet::backdrop { background: rgba(26, 26, 26, 0.45); }
.sheet[open] { animation: pf-sheet-up 200ms var(--ease); }
@keyframes pf-sheet-up { from { transform: translateY(100%); } }
@media (prefers-reduced-motion: reduce) {
  .sheet[open] { animation: pf-xfade 150ms ease; }
}
.sheet .sheet-grab { width: 44px; height: 4px; border-radius: var(--r-pill); background: var(--pf-border); margin: 0 auto var(--s-1); }
/* house close icon: 44px hit area, 30px opaque circle, SVG X dead-centre */
.sheet .close, .lightbox .close, .wl-modal .close {
  width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  background: none; border: 0; padding: 0; cursor: pointer; font-size: 0;
}
.loc-selector .close { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; background: none; border: 0; padding: 0; cursor: pointer; font-size: 0; }
.close .close-x, .hero-expand .close-x {
  width: 30px; height: 30px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--pf-white); border: 1px solid var(--pf-border); color: var(--pf-grey-600);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .15);
  transition: border-color .2s ease-out, color .2s ease-out;
}
.close:hover .close-x, .close:focus-visible .close-x { border-color: var(--pf-orange); color: var(--pf-orange); }

/* Their design inside the card's YOUR-SPACE panel (subtle highlight) */
.cc-panel.your-space.has-art { padding: 0; border-style: solid; outline: 2px solid var(--pf-orange); outline-offset: -2px; }
.cc-panel.your-space.has-art > :not(img.art):not([data-ys-offer]) { display: none; }
.cc-panel.your-space img.art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

@media (max-width: 768px) {
  .hide-mobile { display: none !important; }
  .only-mobile { display: flex; }
  .info-i { display: inline-block; }

  /* 1. Peek strip — sticky under the nav; the artefact never leaves view */
  .peek-strip {
    display: flex; align-items: center; gap: var(--s-2); width: 100%;
    height: 72px; padding: var(--s-1) var(--s-2);
    background: var(--pf-white); border: 1px solid var(--pf-border); border-radius: var(--s-1);
    position: sticky; top: calc(var(--pf-bar-h, 0px) + 60px); z-index: 40;
    cursor: pointer; text-align: left; margin-block: var(--s-1);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
  }
  .peek-strip.pulse { animation: pf-pulse 200ms var(--ease); }
  .peek-thumb {
    flex: 0 0 80px; height: 56px; overflow: hidden; border-radius: 4px;
    border: 1px solid var(--pf-border); position: relative; background: var(--pf-grey-50);
  }
  .peek-thumb > .peek-scale { width: 320px; transform: scale(0.25); transform-origin: top left; }
  .peek-thumb > .peek-scale .cc-flip { display: none; }
  .peek-thumb .solo-mini-frame { position: absolute; inset: 0; }
  .peek-thumb img.solo-mini { width: 100%; height: 100%; object-fit: cover; object-position: top; }
  .peek-mid { flex: 1; min-width: 0; display: flex; flex-direction: column; }
  .peek-t { font-weight: 600; font-size: 15px; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .peek-ctx { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .peek-open { color: var(--pf-orange); font-weight: 700; font-size: 14px; display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; }

  /* THREE-SECTION LAW: on mobile the same three areas are MET ON SCROLL —
     same structure, same labels, same order. The preview no longer hides
     behind the sheet (that arrangement is superseded); the sheet remains
     only as the expanded view. */
  .deck-body { grid-template-columns: 1fr; padding-block: var(--s-2); }
  .deck-preview { display: block; }
  .sheet .deck-preview { display: block; }
  /* DESIGNER-DISPLAY 1 (2026-07-18): on the phone the DESIGNER'S result must
     render in the main flow — nobody chooses a design they can't see. When
     the designer tab is live, the preview column joins the flow BELOW the
     form (generate → result right there), designer stage only. */
  [data-deck="designer"] .deck-preview { display: block; }
  [data-deck="designer"] .deck-preview .artefact-stage > [data-pv]:not([data-pv="designer"]) { display: none; }
  /* B4 STEP 1 — THE 3×2 PREVIEW (Jon-approved spec v2 §1f, first commit).
     The designer pane was ABSOLUTE inside a stage whose height only the
     hidden card pane propped — V1's deletion and the sheet's un-prop rules
     removed the prop, and at phone widths the stage collapsed to its
     padding: the canvas measured 3×2 (the zero-box family's signature).
     THE ARTEFACT OWNS ITS HEIGHT (the flyer's vertical host-box law): on
     the designer tab the pane joins the FLOW and the lead image keeps its
     own render aspect — the stage takes the canvas's height, never a
     sibling's leftover budget. Mobile-scoped: desktop's propped layout
     stands untouched. */
  [data-deck="designer"] .artefact-stage { height: auto; min-height: 0; }
  [data-deck="designer"] .artefact-stage > [data-pv="designer"] { position: static; inset: auto; width: 100%; }
  /* the canvas keeps its ratified aspect (105/74.25, the desktop identity)
     and cover-fills exactly as desktop does — the fix gives it a BOX, not
     a new look */
  [data-deck="designer"] #pf-designer-preview { width: 100%; height: auto; aspect-ratio: var(--pf-cell-ratio-w) / var(--pf-cell-ratio-h); }

  /* 3. Controls compression */
  .deck .cfg-step { padding-block: var(--s-1); }
  .deck .cfg-step .cfg-label { margin-bottom: 6px; }

  .price-block .price-chip { display: inline-block; padding: 8px 14px; margin-top: var(--s-1); }
  .price-block .per-letterbox { display: inline; margin-left: var(--s-1); }
  .price-block .fold-scarcity { margin-top: 6px; }
  .addon-row--combined { justify-content: flex-start; gap: var(--s-1); flex-wrap: wrap; }
  .addon-row--combined .chip { margin-left: auto; }

  /* 4. Sticky CTA (below the dialog top layer; hidden while sheet is open) */
  /* THE BOOKING BUTTON BELONGS WITH THE ORDER (Jon's mobile walk,
     2026-08-09): the in-flow CTA was hidden outright on mobile because the
     sticky bar was "the single booking action" — which left the CONFIRM
     section holding a price and no way to act on it. It shows IN THE RAIL
     (i.e. inside CONFIRM) and nowhere else; the sticky bar stands down
     while it is on screen, so there is still exactly ONE booking action
     visible at any moment. */
  .in-flow-cta { display: none !important; }
  .deck-rail .in-flow-cta { display: block !important; width: 100%; }
  .sticky-cta {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 45;
    padding: var(--s-1) var(--s-2) calc(var(--s-1) + env(safe-area-inset-bottom, 0px));
    background: var(--pf-white); border-top: 1px solid var(--pf-border);
  }
  body.has-sticky-cta { padding-bottom: 88px; }
}

/* ---- Misc visual-first bits ---- */
.h1-accent { color: var(--pf-orange); display: block; }
.hero-meta { display: flex; gap: var(--s-1); align-items: center; flex-wrap: wrap; margin-top: var(--s-2); min-height: 34px; font-size: 15px; font-weight: 600; color: var(--pf-grey-600); }
.mini-ui { width: 100%; height: 100%; padding: var(--s-2); display: flex; flex-direction: column; gap: var(--s-1); justify-content: center; background: var(--pf-white); }
.mini-ui .mu-bar { height: 12px; border-radius: var(--r-pill); background: var(--pf-grey-50); border: 1px solid var(--pf-border); }
.mini-ui .mu-bar.typed { width: 70%; background: var(--pf-white); position: relative; }
.mini-ui .mu-bar.typed::after { content: ""; position: absolute; left: 8px; top: 3px; bottom: 3px; width: 58%; border-radius: var(--r-pill); background: #DED9D2; }
.mini-ui .mu-btn { height: 28px; border-radius: 8px; background: var(--pf-orange); width: 45%; margin-top: var(--s-1); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 11px; font-weight: 800; }
.mini-ui .mu-chips { display: flex; gap: 6px; }
.mini-ui .mu-chip { flex: 1; height: 22px; border-radius: 6px; border: 1px solid var(--pf-border); background: var(--pf-grey-50); }
.mini-ui .mu-chip.on { border-color: var(--pf-orange); background: var(--pf-orange-tint); }
.designer-form .field:last-of-type { margin-bottom: 0; }
.product-visual { border-radius: var(--s-1); overflow: hidden; border: 1px solid var(--pf-border); aspect-ratio: 16 / 10; background: var(--pf-grey-50); }
.product-visual img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }
/* Solo product card: portrait flyer fills the card — no dead white space */
.product-visual--tall { aspect-ratio: auto; flex: 1; min-height: 320px; }
.product-visual--tall img { object-position: center 20%; }


/* ============ Smart Match — map artefact + delivery mode ============ */
.map-art {
  position: relative; aspect-ratio: 4 / 3; overflow: hidden;
  border: 1px solid var(--line, #E7E2DB); border-radius: 12px;
  background: #fff;
}
/* (removed) legacy schematic-era `.map-art svg` sizing — it zeroed Leaflet's
   overlay SVG (panes are 0x0 containers), clipping the heat blob to nothing. */
.map-skel { position: absolute; inset: 0; border-radius: 0; } /* exactly-sized: fills the artefact */
.map-off { display: flex; align-items: center; justify-content: center; height: 100%; padding: var(--s-3); text-align: center; }
.map-note { margin-top: 4px; white-space: normal; } /* locked copy: renders whole, wraps to two lines */
/* NO SENTENCE ENDS IN AN ELLIPSIS (Jon's mobile walk, 2026-08-09): this
   caption used to clip to one line, and whatever landed in it — including
   a refusal that had no business being here — rendered as
   "We could not locate that postcode distri…". A caption may be short;
   it may not be CUT. It wraps, at every width, in every state. */
.meta-slot .card-mock-caption { white-space: normal; overflow: visible; text-overflow: clip; }
#dk-map-cap { margin-top: var(--s-1); min-height: 1.2em; }
#dk-map-cap .text-link { padding: 0; }


/* Stacked-label segmented control (delivery mode) */
.seg--stack { display: grid; grid-template-columns: 1fr 1fr; }
.seg--stack button { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left; padding: 10px 12px; min-height: 58px; }
.seg--stack button small { font-weight: 400; font-size: 12px; line-height: 1.35; color: inherit; opacity: .75; white-space: normal; }

/* ── PRECISION FILTER DECK (Jon's spec 2 Sep; layout is Fable's, for Jon's eye) ──
   Lives INSIDE the Precision box. Its height is set by the deck at open time to
   the room the week picker gives up, so the column's height on desktop is
   unchanged; the six filters scroll within it, one open at a time. Same visual
   language as the column: the field border, the small hint register, brand
   orange only on the active thumb. */
.prec-filters { margin: 0; border: 1px solid var(--pf-border); border-top: 0; border-radius: 0 0 var(--r-btn, 10px) var(--r-btn, 10px); overflow: visible; background: var(--pf-white, #fff); }
.prec-filters[hidden] { display: none; }
/* LEFT COLUMN — EVERYTHING ABOVE THE FOLD, NO INNER SCROLLBAR (Jon-ruled
   2026-09-03). Precision selected: the Smart Match card is ONE switchable line,
   Precision expands in its place and the filter deck hangs off it as one card;
   every control visible at once — small text, tight rows, sliders the full
   column width. Nothing scrolls inside the panel. */
.cfg-step.is-precision #dk-mode { grid-template-columns: 1fr; gap: 6px; }
.cfg-step.is-precision #dk-mode button[data-v="smart"] { flex-direction: row; align-items: center; justify-content: space-between; gap: 8px; padding: 4px 10px; min-height: 0; }
/* while Precision is open the whole column tightens so it stays above the fold (Jon, 3 Sep) */
.zone[data-zone="spec"]:has(.cfg-step.is-precision) .cfg-step { padding-block: 4px; }
.zone[data-zone="spec"]:has(.cfg-step.is-precision) > .zone-lab { padding-top: 5px; padding-bottom: 5px; }
.zone[data-zone="spec"]:has(.cfg-step.is-precision) .pfsh-railcard.pfly-style { padding-top: 10px; padding-bottom: 10px; }
.zone[data-zone="spec"]:has(.cfg-step.is-precision) .cfg-step .hint { margin-top: 2px; }
.cfg-step.is-precision #dk-mode button[data-v="smart"] small { font-size: 11px; opacity: .7; white-space: nowrap; }
.cfg-step.is-precision #dk-mode button[data-v="precision"] { border-bottom-left-radius: 0; border-bottom-right-radius: 0; margin-bottom: 0; padding: 7px 10px; }
.cfg-step.is-precision #dk-mode button[data-v="precision"] small { font-size: 11px; }
/* the settled row is a header while its step is open — a header’s height, not a card’s */
.zone[data-zone="spec"]:has(.cfg-step.is-precision) #dk-settled[aria-expanded="true"] { padding-top: 4px; padding-bottom: 4px; }
.prec-head { display: block; padding: 0 10px; }
.prec-head b { display: block; font-size: 12px; font-weight: 800; letter-spacing: -.1px; }
.prec-date { display: flex; align-items: center; gap: 8px; padding: 0 10px; border-bottom: 1px solid var(--pf-border); }
.prec-date .field { margin: 0; display: flex; align-items: center; gap: 8px; width: 100%; }
.prec-date .field label { font-size: 11px; color: var(--pf-grey-600); white-space: nowrap; margin: 0; }
.prec-date .field input[type="date"] { font: inherit; font-size: 11px; padding: 0 5px; line-height: 1.2; border: 1px solid var(--pf-border); border-radius: 6px; flex: 1 1 auto; min-width: 0; }
/* THE LABEL LINE AND THE TRACK ARE SEPARATE BANDS (Jon's handset walk, 3 Sep):
   the handle is drawn centred on the track, so a track near the top of its band
   put the handle THROUGH the label and the value pair. The track is centred in
   its band, the band is at least as tall as the handle, and the text band has
   real clearance beneath it. */
.prec-row { padding: 1px 10px 0; border-bottom: 1px solid var(--pf-border); }
.prec-row:last-of-type { border-bottom: 0; }
/* THE TEXT DOES NOT TOUCH THE SLIDER (Jon's walk, 3 Sep): the pair above and
   the ends below both came down a point; no padding, position or gap moved, so
   the room this frees is headroom against the single-screenful rule, not spent. */
.prec-line { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: 10.5px; line-height: 1; margin-bottom: 2px; }
.prec-line .prec-v { font-variant-numeric: tabular-nums; color: var(--pf-ink, #14110F); font-weight: 700; white-space: nowrap; }
/* ── THE SLIDER READS AS A SLIDER (Jon-ruled 2026-09-03, presentation polish) ──
   "Currently read as a lone circle; must read unmistakably as a slider, in the
    site's language." A visible track the full width of the column, thin and
    light; the span between the handles filled in brand orange so the position
    is legible at a glance; handles white with an orange border and a soft
    shadow, sitting ON the track; the range's own min and max at its ends; the
    current pair above it; and a touch target far taller than the 4px track.
   HOW THE TARGET IS BIGGER THAN THE TRACK: the two range inputs are
   transparent, absolutely positioned and CENTRED ON THE TRACK, with only their
   thumbs taking pointer events — so the whole band is grabbable, the track and
   the fill can never swallow a press, and neither input blocks the other.
   THE TARGET IS POINTER-AWARE, and that is a choice with a reason (marked in
   DECISIONS-PENDING): 44px on a COARSE pointer, where the 44px rule comes
   from and where the column may scroll; 32px on a fine pointer, because 44px
   on every row at 1280 would break the ruling from item 2 that the whole
   column stays visible — 32px is still more than twice the old target. The
   row pitch always exceeds the target, so two rows' targets never overlap. */
.prec-dual { position: relative; height: 18px; margin: 0; }   /* the handle's own height: it cannot reach out of this band */
.prec-track {
  position: absolute; left: 22px; right: 22px; top: 7px; height: 4px;
  border-radius: 2px; background: var(--pf-border, #E8E8E8); display: block;
}
.prec-fill {
  position: absolute; top: 0; bottom: 0; left: var(--lo, 0%); right: calc(100% - var(--hi, 100%));
  background: var(--pf-orange, #FD3601); border-radius: 2px; display: block;
}
.prec-end {
  position: absolute; top: 5px; font-size: 8.5px; line-height: 1;
  color: var(--pf-grey-600); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.prec-end--lo { left: 0; width: 20px; text-align: right; padding-right: 2px; }
.prec-end--hi { right: 0; width: 20px; text-align: left; padding-left: 2px; }
/* the last row carries no rule — the deck’s own border closes it */
.prec-row:last-of-type { border-bottom: 0; }
.prec-dual input[type="range"] {
  position: absolute; left: 22px; width: calc(100% - 44px); height: 30px; top: -6px;
  margin: 0; padding: 0; background: transparent; border: 0;
  -webkit-appearance: none; appearance: none; pointer-events: none;
}
.prec-dual input[type="range"]:focus { outline: none; }
.prec-dual input[type="range"]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px rgba(253, 54, 1, .28), 0 1px 3px rgba(0, 0, 0, .2); }
.prec-dual input[type="range"]::-webkit-slider-runnable-track { height: 30px; background: transparent; border: 0; }
.prec-dual input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; pointer-events: auto;
  width: 18px; height: 18px; margin-top: 6px; border-radius: 50%;
  background: #fff; border: 2px solid var(--pf-orange, #FD3601);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .2); cursor: pointer;
}
.prec-dual input[type="range"]::-moz-range-track { height: 30px; background: transparent; border: 0; }
.prec-dual input[type="range"]::-moz-range-thumb {
  pointer-events: auto; width: 18px; height: 18px; border-radius: 50%;
  background: #fff; border: 2px solid var(--pf-orange, #FD3601);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .2); cursor: pointer;
}
@media (pointer: coarse) {
  /* the 44px target, centred on the same track; the row grows to keep the
     pitch above the target so no two rows' targets overlap */
  .prec-row { padding-top: 5px; padding-bottom: 6px; }
  .prec-dual { height: 20px; }
  .prec-dual input[type="range"] { height: 44px; top: -12px; }
  .prec-dual input[type="range"]::-webkit-slider-runnable-track { height: 44px; }
  .prec-dual input[type="range"]::-webkit-slider-thumb { width: 20px; height: 20px; margin-top: 12px; }
  .prec-dual input[type="range"]::-moz-range-track { height: 44px; }
  .prec-dual input[type="range"]::-moz-range-thumb { width: 20px; height: 20px; }
}
.prec-note { font-size: 11px; line-height: 1.3; color: var(--pf-grey-600); padding: 4px 10px 6px; margin: 0; }
/* the estimate register never reads as a fact */
.prec-est { font-weight: 400; font-style: italic; opacity: .8; }

/* Peek strip: static tile thumb with blob dot */
.peek-map { position: relative; display: block; width: 56px; height: 42px; border-radius: 6px; overflow: hidden; border: 1px solid var(--line, #E7E2DB); background: #FBFAF8; }
.peek-map img { width: 100%; height: 100%; object-fit: cover; display: block; }
.peek-dot { position: absolute; left: 50%; top: 50%; width: 14px; height: 14px; margin: -7px 0 0 -7px; border-radius: 50%; background: rgba(253, 54, 1, .45); border: 1px solid rgba(253, 54, 1, .7); }

/* ============ Shared card — offer step + zone strip (Smart Match Part 5) ============ */
.offer-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.offer-chips button {
  font: inherit; font-size: 13px; padding: 6px 12px; min-height: 32px;
  border: 1px solid var(--line, #E7E2DB); border-radius: 999px; background: #fff;
  cursor: pointer; transition: border-color .15s ease-out, background .15s ease-out;
}
.offer-chips button:hover { border-color: var(--pf-brand); }
#dk-offer[aria-invalid="true"], #dk-cat-in[aria-invalid="true"] { border-color: var(--pf-brand); }
/* RESERVE-GUIDANCE: incomplete-step lines + the under-CTA summary. Darkened
   brand ramp (not the raw brand orange) so small guide text passes AA. */
.hint.guide { color: #B32500; font-weight: 600; }

/* Engine-rendered offer strip: house style, regardless of artwork source */
.cc-panel .ys-offer {
  border-radius: 0; /* shape rule: square-edged full-width band, never a pill */
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  background: var(--pf-brand); color: #fff;
  font-weight: 800; font-size: 10px; letter-spacing: .01em; text-transform: uppercase;
  padding: 4px 8px; text-align: center;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cc-panel.your-space { position: relative; }

/* Compact zone map: exactly the card's width, one designed stack */
.zone-wrap { margin: var(--s-2) auto 0; width: 100%; max-width: 720px; }
.zone-mini { height: 190px; background: #FBFAF8; }
#dk-zone-cap { text-align: center; margin-top: 4px; }

/* Coming-soon rows with a sub-line (Print only / Design & download) */
.addon-row--sub .ar-main { display: flex; flex-direction: column; gap: 2px; }
.addon-row--sub .ar-sub { font-size: 12px; color: var(--muted, #6B6660); font-weight: 400; }


/* ============ Real map layer (Leaflet + OSM) + expand affordances ============ */
.artefact { position: relative; border: 1px solid var(--line, #E7E2DB); border-radius: 12px; overflow: hidden; background: #fff; }
.artefact > img { display: block; width: 100%; height: auto; }
.deck-preview .fold-solo-art { max-width: 100%; }
.map-wrap { margin-top: var(--s-2); }
.map-art { position: relative; aspect-ratio: 16 / 10; border: 0; border-radius: 0; background: #FBFAF8; }
.leaflet-container { font: inherit; }
.leaflet-container .leaflet-control-attribution { font-size: 10px; background: rgba(255, 255, 255, .8); }
.map-pin { filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .3)); }

.expand-btn {
  position: absolute; top: 8px; right: 8px; z-index: 800; /* above Leaflet panes */
  width: 30px; height: 30px; display: grid; place-items: center;
  background: #fff; border: 1px solid var(--line, #E7E2DB); border-radius: 8px;
  color: #444; cursor: zoom-in; transition: color .15s ease-out, border-color .15s ease-out;
}
.expand-btn:hover, .expand-btn:focus-visible { color: var(--pf-brand); border-color: var(--pf-brand); }

/* Sector breakdown — which homes, exactly */
.sector-breakdown { margin-top: var(--s-2); padding: 10px 12px; border: 1px solid var(--line, #E7E2DB); border-radius: 10px; background: #FBFAF8; line-height: 1.6; }
.sector-breakdown b { display: block; margin-bottom: 2px; }
.sector-breakdown .sb-note { display: block; margin-top: 4px; color: var(--muted, #6B6660); }

/* Lightbox: desktop expand (mobile reuses the bottom sheet) */
.lightbox { border: 0; padding: 0; background: transparent; max-width: none; max-height: none; overflow: visible; margin: auto; }
.lightbox::backdrop { background: rgba(17, 17, 17, .6); }
.lightbox .close { position: fixed; top: calc(8px + env(safe-area-inset-top, 0px)); right: 10px; z-index: 900; }
.lightbox-body { width: min(920px, 92vw); display: flex; flex-direction: column; align-items: center; gap: 12px; max-height: 88dvh; }
.lightbox-body > img { display: block; max-width: 92vw; max-height: 84dvh; width: auto; object-fit: contain; border-radius: 12px; }
.lightbox-body .artefact { width: 100%; max-width: none; margin: 0; }
.lightbox-body .lb-breakdown { width: 100%; margin: 0; }
.lightbox-body .map-art, .lightbox-body .zone-mini { height: min(64vh, 600px); aspect-ratio: auto; }
.lightbox-body .expand-btn { display: none; }
.lightbox-body .lb-breakdown { background: #fff; }
.lightbox[open] { animation: lb-in .2s ease-out; }
@keyframes lb-in { from { opacity: 0; transform: scale(.97); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .lightbox[open] { animation: none; } }

/* Inside the bottom sheet the view IS expanded — no nested expand affordance.
   MOBILE-POLISH-06: the EYE (static IRL shot) is not an expand — it stays,
   same house chip, and takes the hidden expand chip's top-right berth. It
   remains probe-gated: [hidden] must always win over the chip's display. */
.sheet-body .expand-btn:not(.lbx-btn) { display: none; }
.sheet-body .lbx-btn { top: 8px; }
.expand-btn[hidden] { display: none; }


/* ============ Live slot editing: the slot rebuilds as they type ============ */
.your-space .ys-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; text-align: center; }
.your-space .ys-placeholder[hidden], .your-space .ys-live[hidden] { display: none; }
.your-space .ys-live {
  position: absolute; inset: 0; background: #fff;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: 10px; text-align: center;
}
.your-space .ys-name { font-weight: 800; font-size: 12px; letter-spacing: .06em; text-transform: uppercase; color: var(--pf-black, #111); }
.your-space .ys-cat { font-size: 9px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: #8A857D; }
.your-space .ys-headline { font-weight: 900; font-size: 16px; line-height: 1.15; color: var(--pf-brand); overflow-wrap: anywhere; }
.your-space.ys-pulse { animation: ys-pulse .2s ease-out; }
@keyframes ys-pulse { 50% { transform: scale(1.02); } }
@media (prefers-reduced-motion: reduce) { .your-space.ys-pulse { animation: none; } }


/* ============ Layout rhythm batch ============ */
/* Grid blowout fix (pre-existing, caught by this batch's 375 sweep): the deck
   grid tracks inherited min-width:auto from unbreakable seg labels, forcing
   1fr to resolve to 364px inside a 327px container — 13px page overflow on
   every deck page at 375. Tracks and flex items may now shrink to fit. */
.deck-body > *, .deck-panes, .deck-pane, .deck-preview { min-width: 0; }
.seg button { min-width: 0; }
/* INTEGRATION (2026-07-20): the family-engine edit surface — the details
   form reuses the field idiom; the row spaces its actions. */
.dz-editform { margin-top: var(--s-2); border: 1px solid var(--pf-border); border-radius: 10px; padding: var(--s-3); }
.dz-editform .field { margin-top: var(--s-2); }
.dz-editform .field:first-child { margin-top: 0; }
.dz-editform input + input { margin-top: 6px; }
.dz-ef-row { display: flex; gap: var(--s-3); align-items: center; margin-top: var(--s-3); }
.dz-ef-addrow { display: flex; gap: var(--s-2); align-items: center; margin-top: 8px; }
.dz-ef-addrow select { flex: 1; }
.dz-ef-blk { border: 1px solid var(--pf-border); border-radius: 8px; padding: 8px; margin-top: 8px; }
.dz-ef-blk .bhead { display: flex; align-items: center; gap: 8px; font-size: var(--fs-small); font-weight: 700; }
.dz-ef-blk .bhead span { flex: 1; }
.dz-ef-blk .bhead button { background: none; border: 0; cursor: pointer; color: var(--pf-grey-600); font-size: 14px; padding: 2px 6px; }
.dz-ef-blk input, .dz-ef-blk textarea { margin-top: 6px; width: 100%; }
.dz-ef-blk textarea { resize: none; min-height: 62px; }
.dz-ef-row .btn { flex: 1; }

/* QUALITY-1 passing fix (2026-07-19): Portrait/Landscape overflowed at phone
   widths — the pair sits beside a VISUALLY-HIDDEN format seg whose 1fr 1fr
   grid still reserved half the row, and "Landscape" spilled its button. At
   phone widths the orientation seg spans the full row — contained and
   centred like the style buttons above it. Desktop look unchanged. */
@media (max-width: 767px) {
  #dz-orient-wrap { grid-column: 1 / -1; }
}
/* The scarcity chip's long fallback text ("Live availability offline…") was
   nowrap — 364px of unbreakable chip at 375. It may wrap; pill look holds. */
.fold-scarcity .chip { max-width: 100%; white-space: normal; }
.fold-scarcity .pf-counter { max-width: 100%; }
.fold-scarcity .pf-counter .pfc-t { white-space: normal; }

:root { --band: #FAF7F4; } /* warm off-white band — full-bleed, brand-warm */

/* Sticky artefact column (desktop): the card/flyer+map stay in view while the
   controls column scrolls. Bounded by the deck grid, so it can never overlap
   the footer; top offset clears the 56px sticky nav. */



/* ============ ✦ PromoFlyers advisor (recommendation surface v1) ============ */
/* house disclosure: ONE component for every expand/collapse control
   (advisor strip, mobile preview strip, mobile advisor strip) */
span.disclose {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; min-width: 28px; max-height: none; flex: none;
  border-radius: 50%; overflow: visible;
  border: 1px solid var(--pf-border); background: var(--pf-white); color: var(--pf-grey-600);
  transition: transform .2s ease-out, border-color .2s ease-out, color .2s ease-out;
}
.disclose svg { display: block; }
[aria-expanded="true"] .disclose { transform: rotate(180deg); }
button:hover .disclose, button:focus-visible .disclose { border-color: var(--pf-orange); color: var(--pf-orange); }
}
@keyframes adv-pulse { 50% { transform: scale(1.01); } }


/* ============ LAYOUT-STAGE-02: BIG fixed stage (locked design) ============ */
/* The stage never resizes on selection: the landscape card (in flow, width
   calc(100% - 40px) = >=90% of the column, flanks 20px <= 24px) defines the
   stage height; flyer/designer panes are absolute and scale to fit the same
   box. No vh-driven sizing. Caption band (44px) is reserved INSIDE the stage
   so captions can never overlay the artefact. */
.artefact-stage {
  position: relative; width: 100%;
  border: 1px solid var(--line, #E7E2DB); border-radius: 12px;
  background: #fff; overflow: hidden;
  padding: 12px 20px;
}
.artefact-stage > [data-pv] { display: block; }
.artefact-stage > [data-pv="card"] { position: relative; }
.artefact-stage > [data-pv="card"][hidden] { display: block !important; visibility: hidden; } /* keeps the stage height for the designer tab */
/* THE ARTEFACT OWNS ITS HEIGHT (host-box contract, vertical half — Jon-ruled
   2026-08-05 reopen addendum): on the Flyers tab the stage's height derives
   from the PAINTED INSTANCE plus the 44px caption band, never from a hidden
   sibling pane's leftover budget. The tall card pane reserved height the
   landscape flyer never fills (desktop bottom gap), and in the phone sheet —
   where the card pane is display:none — it reserved almost none and crushed
   the instance to 0 (the empty sheet zoom, the mid-sentence caption cut). */
.deck[data-deck="solo"] .artefact-stage > [data-pv="card"][hidden] { display: none !important; }
.sheet .artefact-stage > [data-pv="card"][hidden] { display: none !important; }
.artefact-stage > [data-pv="solo"] {
  position: relative; display: block; width: 100%;
  padding-bottom: 44px; box-sizing: border-box;
}
/* THREE-SECTION LAW: the DESIGN zone is its own column now — there is no
   sibling rail to prop the stage's height, and the absolute designer pane
   contributed none, so the mockup vanished from the middle. THE ARTEFACT
   OWNS ITS HEIGHT (the same law the phone already obeyed): the designer
   pane joins the FLOW at every width and the stage takes the canvas's
   height. The hidden card pane's height prop retires with it. */
.artefact-stage > [data-pv="designer"] {
  position: relative; inset: auto;
  display: flex; align-items: center; justify-content: center;
}
.deck[data-deck="designer"] .artefact-stage > [data-pv="card"][hidden] { display: none !important; }
.artefact-stage > [data-pv="solo"][hidden],
.artefact-stage > [data-pv="designer"][hidden] { display: none; }
.artefact-stage [data-composed-card] { width: 100%; max-width: none; }
/* one masthead scale that never wraps from ~450px to ~1250px card widths */
.artefact-stage .cc-mast { padding: 10px 14px; }
.artefact-stage .cc-mast .m-quiet { font-size: 20px; }
/* the stage overlays its eye/expand controls on the card top-right — the
   quiet line clears them (stage-only; print and lightbox carry no chrome) */
.artefact-stage .cc-mast .m-line { padding-right: 40px; }
.artefact-stage .designer-preview {
  max-width: 100%; max-height: calc(100% - 44px); /* caption band reserved */
  width: auto; height: auto; object-fit: contain; margin-bottom: 44px;
}
.artefact-stage .designer-preview { aspect-ratio: var(--pf-cell-ratio-w) / var(--pf-cell-ratio-h); height: calc(100% - 44px); border: 1px solid var(--line, #E7E2DB); }
.artefact-stage .stage-cap {
  /* wraps, never clips — "No design yet — the Designer or your upload
     lands here." was ending in an ellipsis at 390 (no-truncation law) */
  position: absolute; left: 0; bottom: 10px; margin: 0;
  max-width: calc(100% - 8px); white-space: normal; overflow: visible; text-overflow: clip;
  background: rgba(255,255,255,.92); border: 1px solid var(--line, #E7E2DB);
  border-radius: 8px; padding: 4px 10px;
}
/* DESIGNER-01: pane = preview / narration line / alternates strip, every row
   reserved from first paint - the stage cannot move during generation */
.artefact-stage > [data-pv="designer"] { flex-direction: column; gap: 6px; }
.artefact-stage [data-pv="designer"] .designer-preview { margin-bottom: 0; }
/* STAGE-3 item 2: a clipped placeholder must never read as a glitch —
   any overflow ellipsizes instead of chopping mid-word ("St") */
input::placeholder, textarea::placeholder { text-overflow: ellipsis; }
/* FIT-1B §1b: the prefilled door's filled, tappable summary */
.dz-sum { display: grid; gap: 6px; margin-bottom: var(--s-2); }
.dz-sum-hint { font-size: var(--fs-small); color: var(--pf-grey-600); margin: 0 0 2px; }
.dz-sum-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  width: 100%; text-align: left; cursor: pointer;
  background: var(--pf-grey-50); border: 1px solid var(--pf-border); border-radius: 8px;
  padding: 10px 12px; font: inherit;
}
.dz-sum-row:hover, .dz-sum-row:focus-visible { border-color: var(--pf-brand); }
.dz-sum-label { font-size: 12px; font-weight: 700; color: var(--pf-grey-600); white-space: nowrap; }
.dz-sum-val { font-size: 14px; font-weight: 600; color: var(--pf-black); overflow-wrap: anywhere; }
.dz-sum-val i { font-style: normal; font-weight: 500; color: var(--pf-grey-600); }
/* DESIGNER-FIT §4 (ratified): narration promoted — big, orange, unmissable
   at 390; real stages only ("Stage N of 4" from data-stage, never fake %) */
.dz-narr {
  min-height: 26px; margin: 0; font-size: 19px; line-height: 1.3; font-weight: 800;
  color: var(--pf-brand); text-align: center;
}
.dz-narr:not(:empty)[data-stage]::after {
  content: attr(data-stage); display: block; font-size: 12px; font-weight: 600;
  color: var(--pf-grey-600); letter-spacing: 0.04em;
}
/* DESIGNER-DISPLAY 6: honest placeholder labelling — stock/blank imagery
   never reads as the customer's own design */
.designer-preview.dz-blank::before {
  content: 'Yours appears here after generate.';
  position: absolute; left: 0; right: 0; bottom: 10px; z-index: 3;
  text-align: center; font: 600 12px/1.4 Inter, system-ui, sans-serif;
  color: var(--pf-grey-600);
}
.dz-eg-label {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: 10px; z-index: 6;
  background: rgba(255, 255, 255, 0.92); border-radius: var(--r-pill);
  padding: 3px 12px; font: 600 11px/1.4 Inter, system-ui, sans-serif;
  color: var(--pf-black); white-space: nowrap;
}
/* the branded flight over the stage while the engine works */
#dz-flight { position: absolute; inset: 0; z-index: 6; display: flex; align-items: center; justify-content: center; pointer-events: none; }
#dz-flight img { width: 64px; height: auto; animation: pf-fly 2.6s ease-in-out infinite; filter: drop-shadow(0 6px 14px rgba(26, 26, 26, 0.18)); }
@keyframes pf-fly {
  0% { transform: translate(-16px, 9px) rotate(-6deg); }
  50% { transform: translate(16px, -9px) rotate(5deg); }
  100% { transform: translate(-16px, 9px) rotate(-6deg); }
}
.designer-preview.dz-generating img:not(.dz-b) { opacity: 0.22; }
/* DESIGNER-FIT §1 (ratified): before any design lands, "AI design it" is THE
   primary on screen — Reserve resumes primacy once a design exists. One
   tasteful attention treatment: a shimmer that passes three times, then rests. */
/* FLOW RULING — BOOKING-FIRST ON BOTH PRODUCTS (Jon's designer-test
   ruling 4, 2026-08-07, SUPERSEDES DESIGNER-FIT §1's designer-CTA
   promotion): the primary flow is book and pay, THEN design — the
   Reserve/booking CTA keeps primacy at every stage, and the design
   door stays open in its normal secondary dress, never promoted over
   the sale. The data-pf-design attribute remains as STATE (probes and
   the bar read it); it no longer drives CTA hierarchy. */
@media (prefers-reduced-motion: reduce) {
  #dz-flight img { animation: none; }
}
.dz-strip {
  height: 64px; display: flex; gap: 8px; align-items: center; justify-content: center;
  width: 100%; max-width: 420px;
}
.dz-strip button {
  width: 76px; height: 56px; padding: 2px; border-radius: 8px;
  border: 2px solid transparent; background: var(--pf-grey-50); cursor: pointer;
  overflow: hidden; position: relative; opacity: 0; transform: translateY(6px);
  transition: border-color .2s ease-out, opacity .25s ease-out, transform .25s ease-out;
}
.dz-strip button.dz-in { opacity: 1; transform: none; }
.dz-strip button[aria-pressed="true"] { border-color: var(--pf-brand); }
.dz-strip button img { width: 100%; height: 100%; object-fit: contain; display: block; }
.dz-strip button .dz-slot-busy { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,.72); }
.dz-strip:empty { visibility: hidden; } /* box reserved, invisible until first generation */

/* theatre: the artefact fades in whole; region covers dissolve/slide away in
   sequence (image -> headline -> offer strip -> contact). Covers sit over the
   REAL render - assembly theatre, never a fake artefact. */
.designer-preview { position: relative; }
.designer-preview > img { width: 100%; height: 100%; object-fit: contain; display: block; }
.designer-preview img.dz-b { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; opacity: 0; transition: opacity .25s ease-out; z-index: 1; pointer-events: none; }
.designer-preview img.dz-b.dz-show { opacity: 1; z-index: 3; }
.designer-preview img.dz-lead-in { animation: dz-image-fade 450ms ease-out; }
@keyframes dz-image-fade { from { opacity: 0; filter: saturate(.6); } }
.dz-covers { position: absolute; inset: 0; }
.dz-cover {
  position: absolute; background: rgba(14, 12, 10, .38);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  transition: opacity .3s ease-out, transform .3s ease-out;
}
.dz-cover.dz-off { opacity: 0; }
.dz-cover[data-r="offer-strip"].dz-off { transform: translateX(14%); }
.dz-cover[data-r="contact"].dz-off { transform: translateY(60%); }
@media (prefers-reduced-motion: reduce) {
  .dz-cover { transition: opacity .15s ease; }
  .dz-cover.dz-off { transform: none; }
  .designer-preview img.dz-lead-in { animation: none; }
}

/* THE LOOK PICKER (Jon's ruling #11): real specimens, tap one —
   flagship leads; visible only when the flyer context is known */
.dz-looks { display: flex; gap: 10px; overflow-x: auto; padding: 2px; }
.dz-looks button { flex: 0 0 auto; padding: 3px; border: 2px solid var(--pf-border); border-radius: 10px; background: #fff; cursor: pointer; }
.dz-looks button[aria-pressed="true"] { border-color: var(--pf-brand); }
.dz-looks img { display: block; border-radius: 6px; } /* size is DERIVED further down — see "the carousel pays first" */
/* THE ROWS ALWAYS SCROLL (Jon's gallery ruling 2026-08-09): the wide-screen
   "wrap into a wall" is retired — a wrapped gallery grew to 1499px tall and
   pushed the design off the screen, which is the thing the row exists to
   prevent. One row per face, scrolled, thumbs at a seeable size. */
/* THREE-SECTION LAW: below the mockup the gallery is a SCROLLABLE ROW
   (Jon's v2 words — "a scrollable gallery"), never a wall that pushes the
   design off the screen. One row, snapped, the thumbs stay seeable. */

/* tap-to-edit regions: invisible targets over the real render (no chrome
   until focus/hover); the editor is the only visible edit UI */
/* THE WALK'S COVER CATCH (2026-08-09): the preview images painted
   ABOVE the region layer — a finger could never reach a tap-to-edit
   target on the flagship render. The layer now sits above the imgs
   (editor stays higher at z5); the CONTAINER is click-transparent so
   only the buttons take the finger. */
.dz-regions { position: absolute; inset: 0; z-index: 4; pointer-events: none; }
/* ── THE EDIT AFFORDANCE IS VISIBLE (Jon-ruled 2026-08-10) ─────────────
   Tap-to-edit is the PRIMARY way a customer fills their flyer, so the
   targets can no longer be invisible until hovered — on a phone there is
   no hover, and an invisible target is a feature nobody finds. Every
   editable region wears a faint standing outline; touching it lights it
   up in the brand register.
   THE FINGER FLOOR: a region smaller than 44px carries a MAGNIFIED hit
   area grown from its own centre (--rg-x/--rg-y, set from the painted
   box), while the outline stays on the true text — the code chip is a
   12px glyph and a 44px target at the same time. */
/* EACH REGION READS AS ITS OWN FIELD (Jon's closing walk, 2026-08-11:
   "the lines in the main copy don't look individually clickable"). The
   affordance existed — a 1.5px dash at 28% — but the face paints small, so
   at that scale the dashes read as noise and adjacent lines merge into one
   another. Three changes, all aimed at SEPARATION rather than decoration:
   a brand-tinted ground so a region is a field rather than an outline, a
   solid edge that does not dissolve at small sizes, and a ring that sets
   each region off from the one above it. The flyer is still the thing being
   looked at — this has to say "tap here" without competing with the design
   it sits on. */
.dz-regions button {
  position: absolute; background: rgba(253, 54, 1, .07);
  border: 1px solid rgba(253, 54, 1, .40);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .55);   /* separates neighbours */
  border-radius: 4px; cursor: text; padding: 0; pointer-events: auto;
  transition: border-color .15s ease-out, background .15s ease-out;
}
/* and a mark that says EDITABLE, not merely outlined — dropped on regions
   too small to carry it, where the tint alone does the work */
.dz-regions button::before {
  content: "";
  position: absolute; top: 1px; right: 1px; width: 5px; height: 5px;
  border-radius: 50%; background: var(--pf-brand); opacity: .75;
}
.dz-regions button.dz-rg-small::before { display: none; }
/* ── AN EMPTY IMAGE SLOT SHOWS AN IMAGE ICON WITH A PLUS (Jon-ruled
   2026-08-24: "not a blank space, so a customer knows something goes there")
   ─────────────────────────────────────────────────────────────────────────
   The artwork draws an empty slot as an empty box, which says nothing to
   anyone who has not been told. The mark is drawn on the REGION, not into the
   artwork: it is an affordance, not print, so it can never reach a PDF — and
   the zoom paints the same layer, which is how it appears there too without a
   second copy.
   It is a chip, not a wash: white behind the glyph so it reads on any
   artwork, dark or light, and centred so it cannot be mistaken for content.
   The glyph is one inline SVG (a picture with a plus), so it stays crisp at
   every size the slot takes. */
.dz-regions button[data-empty="1"] {
  background: rgba(253, 54, 1, .10);
  border-style: dashed; border-color: var(--pf-orange, #FD3601);
}
.dz-regions button[data-empty="1"]::after {
  content: "";
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  /* SIZED BY THE BAND'S SHORT EDGE, NOT ITS WIDTH. Walked on a generated
     design: the back's logo band measures 107x13, and a chip sized from the
     WIDTH (46% of 107 = 49px) would have stood four times the height of the
     band it is marking. Height first, capped at 40px, square by ratio — so it
     fits a thin band and still reads on a generous one. */
  height: min(78%, 40px); width: auto; aspect-ratio: 1; border-radius: 50%;
  background: rgba(255, 255, 255, .92) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23FD3601' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.5' y='4.5' width='14' height='11' rx='1.6'/%3E%3Ccircle cx='6.6' cy='8.4' r='1.2'/%3E%3Cpath d='M3.2 13.2l3.4-3 3 2.6 2.4-2 3.8 3.4'/%3E%3Cpath d='M18.5 15v6M15.5 18h6'/%3E%3C/svg%3E") center / 64% no-repeat;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .22);
  pointer-events: none;
}
/* below about 26px there is no room for a chip; the dashed brand edge is the
   whole message there, the way the tint alone carries a band too small for a
   dot */
.dz-regions button[data-empty="1"].dz-rg-small::after { display: none; }
.dz-regions button:hover, .dz-regions button:focus-visible, .dz-regions button:active {
  border-color: var(--pf-brand); background: rgba(253, 54, 1, .18);
  box-shadow: 0 0 0 2px rgba(253, 54, 1, .22);
}
/* THE GROWN TARGET IS GONE (Jon-ruled 2026-08-14). A target is its band.
   Zoom is the route for a band too thin to tap, and it derives its own floor
   per width, so every band reaches 44px at a step the customer can reach.
   The expansion could never reach 44px on contiguous bands anyway, and at zoom
   it stole neighbours' centres because it had been apportioned against the
   unzoomed geometry. Deleted rather than disabled. */
.dz-editor {
  position: absolute; z-index: 5; left: 50%; transform: translateX(-50%);
  background: var(--pf-white); border: 1px solid var(--pf-border); border-radius: 10px;
  box-shadow: 0 6px 24px rgba(0,0,0,.16); padding: 10px; width: min(320px, 92%);
  display: flex; flex-direction: column; gap: 8px;
}
.dz-editor[hidden] { display: none; }
/* ON THE FACE TOO, THE EDITOR IS A SIBLING OF THE PICTURE, NOT A LAYER OVER
   IT (walk, 2026-08-22). Unzoomed, the popover sat over the bands beneath the
   one being edited, so a second band could not be reached until the first
   was saved — and "Add your own picture" under it got the editor's input.
   The zoom already had this ruling (2026-08-15); the face takes the same
   answer: the editor lays out BELOW the artwork and the page makes room. */
.dz-face-prev:has(> .dz-editor:not([hidden])) > .dz-editor {
  position: static; transform: none; z-index: 1;
  width: min(420px, 96%); margin: 10px auto 0;
}
/* ── THE DIALOG HOLDS BOTH, OR IT IS NOT SHOWING EITHER (Jon-ruled
   2026-08-15) ─────────────────────────────────────────────────────────────
   In the zoom the editor opened BELOW the flyer, inside a face that clips —
   so the second field was cut off by the dialog edge and the contact band was
   obscured. "Some cuts the dialogue."

   Derived from the space, exactly as the face already is: with the editor
   open the panel is a column of [flyer][editor], the editor takes what it
   needs, and the flyer takes what is left. The picture is contained, so a
   shorter box is a smaller flyer — nothing clipped, nothing scrolled, and no
   height typed anywhere. */
/* THE COLUMN WAS DESCRIBED AND NEVER DECLARED. .dz-zoom is display:flex with
   the default ROW direction, so "a column of [flyer][editor]" was in the
   comment and not in the layout — the pair have been sitting side by side,
   which is why the panel was a squeeze at every height rather than a clip at
   some of them. Declared here, and the row becomes the deliberate answer to a
   SHORT viewport below. */
.dz-zoom[data-editing="1"] { flex-direction: column; align-items: center; }
.dz-zoom[data-editing="1"] > .dz-editor {
  position: static; transform: none; z-index: 1;
  flex: 0 0 auto; width: min(420px, 96%); margin: 10px auto 0;
}
.dz-zoom[data-editing="1"] .dz-zoom-face { flex: 1 1 auto; min-height: 0; }
/* AND THE CLIP IS IMPOSSIBLE BY CONSTRUCTION, NOT BY MARGIN (Jon-ruled
   2026-08-15). The hoist out of the clipping face happens on the open path,
   but at 1534x889 the column left FIFTEEN PIXELS of headroom — a fix that
   holds by luck at the heights we happened to measure. Two rules make it
   structural instead:
   1. an editor that is still inside the face is laid out as a sibling
      anyway, so a path that forgets to hoist cannot produce a cut field;
   2. when the height is short and the width is not, the pair sits SIDE BY
      SIDE — the same "derive it from the space" answer the face uses, applied
      to the dimension that ran out. A laptop at 1440x600, a phone on its
      side: both have width to spare and no height at all. */
.dz-zoom[data-editing="1"] .dz-zoom-face > .dz-editor,
.dz-zoom[data-editing="1"] .dz-editor {
  position: static; transform: none; z-index: 1; flex: 0 0 auto;
}
@media (max-height: 760px) and (min-width: 700px) {
  .dz-zoom[data-editing="1"] { flex-direction: row; align-items: stretch; gap: 12px; }
  .dz-zoom[data-editing="1"] > .dz-editor {
    margin: 0; align-self: center; width: min(380px, 42%);
  }
  .dz-zoom[data-editing="1"] .dz-zoom-face { flex: 1 1 auto; min-width: 0; }
}

.dz-editor input { width: 100%; padding: 8px 10px; font: 400 15px/1.4 Inter, system-ui, sans-serif; border: 1px solid var(--pf-border); border-radius: 8px; }
.dz-editor textarea { width: 100%; padding: 8px 10px; font: 400 15px/1.4 Inter, system-ui, sans-serif; border: 1px solid var(--pf-border); border-radius: 8px; resize: vertical; }
.dz-editor textarea[hidden] { display: none; }
.dz-ed-hint { font: 400 12px/1.4 Inter, system-ui, sans-serif; color: var(--pf-muted, #666); }
.dz-ed-hint[hidden] { display: none; }
.dz-ed-row { display: flex; gap: 10px; align-items: center; }
.dz-ed-row .btn { padding: 8px 14px; }

/* quiet tools row + nudge under the commit button */
.dz-tools { display: flex; gap: 8px; align-items: center; justify-content: center; margin-top: var(--s-1); font-size: 13px; }
.dz-tools[hidden] { display: none; }
.dz-tools .text-link[disabled] { opacity: .5; pointer-events: none; }
.dz-nudge { margin-top: var(--s-1); }
.map-slot, .zone-wrap, .map-wrap { isolation: isolate; } /* Leaflet z-indexes never escape */
/* AND THE MAP SLOT DOES NOT RESERVE A MAP'S WORTH OF SPACE FOR A MAP THAT HAS
   NOT LOADED (Jon-ruled 2026-08-24). The height is fixed so a map that IS
   coming does not shove the rail around as it arrives — but when both artefact
   wraps are hidden, or the slot is carrying the offline sentence, there is no
   map to reserve for and the fixed height is 340px of nothing. */
.map-slot:not(:has(> .artefact:not([hidden]))) { height: auto; min-height: 0; }
.map-slot[data-map="offline"] { height: auto; min-height: 0; }
.map-slot[data-map="offline"] .artefact { position: static; height: auto; }
.map-slot { margin-top: 8px; height: clamp(300px, 24vw, 420px); position: relative;
  border: 1px solid var(--line, #E7E2DB); border-radius: 12px; background: #FBFAF8; overflow: hidden; }
.map-slot .zone-wrap, .map-slot .map-wrap { position: absolute; inset: 0; width: 100%; max-width: none; margin: 0; border: 0; border-radius: 0; height: 100%; }
.map-slot .zone-mini, .map-slot .map-art { height: 100%; aspect-ratio: auto; }
/* ── PRESENCE WITHOUT CONTENT, THREE OF THEM (Jon-ruled 2026-08-24) ────────
   The caption slot reserved 48px on SOLO with its only child hidden — the
   words exist ("Where this PromoFlyer lands — BS3") and belong to Shared, so
   on solo the slot stood empty and spent 48px saying nothing. It keeps its
   height only when something in it is showing. Whether solo's map should have
   a caption of its own is Jon's: writing one here would be inventing customer
   copy. */
.meta-slot { min-height: 0; margin-top: 0; }
.meta-slot:has(> :not([hidden])) { min-height: 48px; margin-top: 6px; }
.meta-slot > p:empty { display: none; }
/* THREE-SECTION LAW: in the CONFIRM zone the delivery truth is a full
   sentence — the old fixed 48px slot cut "…verified by" mid-line */
.deck-rail .meta-slot { height: auto; overflow: visible; }
.meta-slot p { margin: 0 0 2px; }
.deck-rail .sector-breakdown { padding: 6px 10px; font-size: 12px; line-height: 1.45; margin-top: 8px; }

/* SLOT TEXT FIT-TO-BOUNDS: live-slot text scales with its panel (container
   query units) and wraps — no truncation at any render size. */
.cc-panel.your-space { container-type: inline-size; }
.your-space .ys-label { font-size: clamp(10px, 9cqi, 22px); white-space: normal; overflow-wrap: anywhere; }
.your-space .ys-sub { font-size: clamp(7px, 5cqi, 13px); }
.your-space .ys-name { font-size: clamp(9px, 7cqi, 16px); overflow-wrap: anywhere; }
.your-space .ys-headline { font-size: clamp(10px, 8.5cqi, 22px); }
.cc-panel .ys-offer { white-space: normal; overflow: visible; text-overflow: clip; font-size: clamp(7px, 5.5cqi, 12px); line-height: 1.25; }

}
}


/* ============ Engine v1.5: variants strip, mini segs, advisor handoff ============ */
.dz-variants { display: flex; gap: 8px; margin-top: 10px; }
.dz-variants button {
  flex: 1; min-width: 0; padding: 4px; background: #fff; cursor: pointer;
  border: 1px solid var(--line, #E7E2DB); border-radius: 8px; font: inherit; font-size: 11px;
}
.dz-variants button[aria-pressed="true"] { border-color: var(--pf-brand); }
.dz-variants img { width: 100%; height: 64px; object-fit: cover; border-radius: 5px; display: block; }
.dz-variants span { display: block; padding: 3px 0 1px; color: var(--muted, #6B6660); text-transform: capitalize; }
.seg--mini button { padding: 6px 10px; font-size: 12px; }

/* Peek-strip plane marker (mini logo mark over the tile thumb) */
.peek-plane { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); line-height: 0; filter: drop-shadow(0 1px 2px rgba(0,0,0,.35)); }
.map-pin { filter: drop-shadow(0 1px 2px rgba(0,0,0,.35)); }
.map-pin img { width: 40px !important; height: 40px !important; max-width: none; display: block; } /* the width attribute loses to img-sizing rules — pin it */

/* Peek thumb: Smart Match blob state carried onto the static tile */
.peek-blob { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); border-radius: 50%;
  background: radial-gradient(circle, rgba(253,54,1,.4), rgba(253,54,1,.18) 55%, transparent 72%); }


/* ============ LAYOUT-STAGE-04: balanced rail (>=1600px) ============ */
/* ONE gap token between all blocks: stage<->rail, map<->advisor<->pricing. */
:root { --stage-gap: 16px; }
/* Stage left (~60%), rail right: map (fixed) -> advisor strip -> PRICING
   summary card. Stage and rail are one grid, top AND bottom edges aligned
   (stretch): the pricing card absorbs the rail's remaining height, so there
   is no dead space under the advisor. Below 1600px: stacked, unchanged. */
/* LOOK-RESTORE-02 s1: the SAME balanced-rail layout also applies on the
   SHORT-VIEWPORT TIER (laptops — 1366x768, 1536x760, scaled-1080): same
   layout, same rules; the tier's smaller FIXED constants follow below.
   Fixed geometry per tier — never vh-fluid (layout law). */
@media (min-width: 1200px) {
  /* the stage is the design zone's own artefact now — the rail moved OUT
     to the CONFIRM zone, so the old nested stage|rail split is retired */
  .deck-preview { display: block; }
  .deck-preview .artefact-stage {
    display: flex; flex-direction: column; justify-content: center; }
  .deck-rail .map-slot { margin-top: 0; height: 340px; }
  /* ...AND NOT WHEN THERE IS NO MAP TO RESERVE FOR (Jon-ruled 2026-08-24). The
     rule above is the one that actually sets 340px in the rail, so the offline
     case has to be answered HERE, at its specificity — written only against
     `.map-slot` it lost, and the walk measured 340px of offline sentence with
     the fix deployed. Third time this file has taught me the same lesson in a
     day: check which rule WINS, not which rule is correct. */
  .deck-rail .map-slot[data-map="offline"],
  .deck-rail .map-slot:not(:has(> .artefact:not([hidden]))) { height: auto; min-height: 0; }
  .deck-rail .meta-slot { margin-top: 0; }
  /* captions compact at rail width so the fixed 48px slot holds the longest
     state (the over-tier warning) without clipping */
  .deck-preview .meta-slot p { font-size: 12px; line-height: 1.3; margin-bottom: 1px; }
  .deck-preview .pricing-slot { grid-area: pricing; }
  .deck-preview .sector-breakdown { grid-area: breakdown; margin-top: 0; align-self: start; }
  /* All three tab groups stack in ONE cell: the tallest defines the card
     height, so geometry is identical on every tab and never responds to the
     text inside — only the visible group changes. */
  .pricing-slot {
    display: grid; min-height: 0;
    border: 1px solid var(--line, #E7E2DB); border-radius: 12px;
    background: #fff; padding: 10px 16px;
  }
  .pricing-slot[hidden] { display: none; }
  .pricing-slot .rail-price { grid-area: 1 / 1; display: flex; flex-direction: column; min-height: 0; }
  .pricing-slot .rail-price[hidden] { display: flex; visibility: hidden; pointer-events: none; }
  /* every price row reserves the filled-state height - lines appearing or
     clearing (save line, exclusive) change text, never edges */
  .pricing-slot .price-lines .pl { min-height: 28px; }
  /* the CTA anchors to the card's bottom edge; only text above it changes */
  .pricing-slot .rail-price > .btn { margin-top: auto !important; }
  /* ── AND IT KEEPS ITS CLEARANCE ABOVE WHILE IT ANCHORS (Jon-ruled 2026-08-12,
     landed here 2026-09-02) ──────────────────────────────────────────────────
     The 12 Aug ruling put 18px above "Book my drop" (`#dk-solo-cta { margin-top:
     18px }`, further down) and this `auto !important` overrode it from 1200 up.
     `auto` is "all the slack there is", and a full column has none: measured
     0px at 1440 and 1600, the button flush against the line above it, exactly
     the "last item in the field group" the ruling was written against. It
     read 22px at 390 and 1024 only because this block does not apply there.
     A column cannot say max(18px, auto), so the floor is its own flex item —
     an 18px spacer ordered just before the button — and the button's auto
     margin still takes whatever slack remains above it. The edge stays where
     it was; the gap can no longer be nothing. */
  .pricing-slot .rail-price::before { content: ''; flex: 0 0 18px; order: 998; }
  .pricing-slot .rail-price > .btn { order: 999; }
  .pricing-slot .price-block { margin-top: 0; }
  .pricing-slot .price-lines { margin-top: 0; }
  .pricing-slot .rp-dz-ctx { margin: 0; min-height: 18px; }
  .pricing-slot .rp-dz-status { margin: 6px 0 0; font-size: 13px; font-weight: 600; min-height: 18px; }
  .pricing-slot .rp-dz-edits { margin: 4px 0 0; font-size: 12px; line-height: 1.35; }
  /* hand-back note anchors the card's bottom edge (with the CTA once a
     design exists) - the panel reads dressed, never a dead block */
  .pricing-slot .rp-dz-note { margin: 4px 0 0; font-size: 12px; line-height: 1.35; margin-top: auto; }
  .pricing-slot .rail-price[data-rp="designer"] > .btn { margin-top: 8px !important; }
  .pricing-slot.over-tier .price-lines { outline: 2px solid #C22C00; outline-offset: 4px; border-radius: 6px; }
  /* solo pane: six steps compress to sit inside the 900px fold */
  .deck-pane[data-pane="solo"] .cfg-step { padding-block: 8px; }
  .deck-pane[data-pane="solo"] .seg--stack button { min-height: 50px; padding: 7px 12px; }
  /* card-tab scarcity: chip becomes two reserved LINES in the rail card.
     MOBILE-POLISH-06: the counter row is now ONE row (counter + bar + ⓘ) —
     at rail width the counter text hides (the spaces line carries it) and
     the bare bar + ⓘ remain, exactly the old dk-avail-row presentation. */
  .pricing-slot .price-block .fold-scarcity .pf-counter { display: none; }
  .pricing-slot .price-block .fold-scarcity > .dk-segs { display: inline-flex; }
  .pricing-slot .price-block .scarcity-line { display: block; margin: 0; min-height: 20px; }
  .pricing-slot .price-block .scarcity-line--closes { color: var(--pf-orange); font-weight: 700; }
  /* the docked pane keeps its shape without the (now railed) price panel */
  .deck-pane .rail-home-marker { display: none; }
}
/* FACE-CALM-01: the 1200–1599 width band ALWAYS gets the rail layout; the
   viewport height only picks the CONSTANT. >820px heights (1536x864
   scaled-1080 laptops — the hole the old height-gated condition left in the
   stacked layout) get the roomier fixed constant. */
@media (min-width: 1200px) and (max-width: 1599px) and (min-height: 821px) {
  .deck-preview { grid-template-columns: 480px minmax(0, 1fr); }
  .deck-preview .map-slot { height: 240px; }
  .deck-preview .meta-slot { height: 62px; }
  .deck-preview .meta-slot p { font-size: 11px; }
  .artefact-stage .cc-mast .m-quiet { font-size: 16px; }
}
/* LOOK-RESTORE-02 s1: SHORT-VIEWPORT TIER constants (height <= 820px below
   the 1600 tier). Stage column FIXED at 440px -> ~400px card -> stage
   bottom ~668px: decision block + primary CTA sit above the fold at
   1366x768 AND 1536x760. Map shrinks with the tier. */
@media (min-width: 1200px) and (max-width: 1599px) and (max-height: 820px) {
  /* FACE-CALM-01: the fold is measured UNSCROLLED (returning visitor),
     which costs the pre-deck offset — the tier's fixed constants (stage
     column + map) give those pixels back so decision block + CTA sit above
     the 760/768 fold without scroll. */
  .deck-preview { grid-template-columns: 420px minmax(0, 1fr); }
  .deck-preview .map-slot { height: 120px; }
  /* the rail floor is the PRICING card reserved min-content (tallest tab
     group) — the tier compresses its rows to clear the unscrolled fold */
  .pricing-slot { padding: 8px 14px; }
  .pricing-slot .price-lines .pl { min-height: 23px; }
  .pricing-slot .price-block .scarcity-line,
  .pricing-slot .price-block .pro-line { min-height: 16px; line-height: 16px; font-size: 11px; }
  .pricing-slot .rp-dz-status, .pricing-slot .rp-dz-ctx { min-height: 16px; }
  /* the tier's narrower rail wraps the caption to three lines — the fixed
     48px slot grows to hold it (fold headroom is ~100px at this tier) */
  .deck-preview .meta-slot { height: 62px; }
  .deck-preview .meta-slot p { font-size: 11px; }
  .artefact-stage .cc-mast { padding: 8px 12px; }
  .artefact-stage .cc-mast .m-quiet { font-size: 15px; }
}

/* ============ LAYOUT-STAGE-03: mobile polish ============ */
/* (c) all three tabs fit at 375 — short labels under 480px */
/* B-FIX-01: full labels restored everywhere; phones size DOWN to fit all
   three at 360 (measured; short forms remain the automatic fallback only
   for Solo/Designer if a translation ever overflows) */
.deck-tabs .tab-short { display: none; }
@media (max-width: 479px) {
  .deck-tabs { gap: 6px; }
  .deck-tabs button { white-space: nowrap; font-size: 12.5px; padding: 8px 7px; letter-spacing: -0.01em; }
  .deck-tabs .sparkle { width: 11px; height: 11px; }
}
/* MOBILE-TIDY-03: captions in the sheet wrap cleanly - never mid-line */
/* (the sheet's solo pane is in-flow block now — the artefact-owns-height
   contract; the old column-flex rule died with the absolute pane) */
.sheet .artefact-stage .stage-cap {
  position: static; margin: 0 auto; max-width: 100%;
  white-space: normal; overflow: visible; text-overflow: clip; text-align: center;
}
.sheet #dk-meta { height: auto; overflow: visible; }
.sheet .meta-slot .card-mock-caption { white-space: normal; overflow: visible; text-overflow: clip; }

/* (d) sheet close: small, FIXED top-right of the sheet viewport, above all
   content, safe-area aware */
.sheet .close { position: absolute; top: calc(2px + env(safe-area-inset-top, 0px)); right: 2px; z-index: 900; }
/* (f) designer preview: contain BOTH axes like the solo flyer (the height ->
   width derivation overflowed narrow containers on device) */
.artefact-stage .designer-preview {
  height: auto; width: auto;
  max-width: 100%; max-height: calc(100% - 44px);
}
/* (g) empty-slot placeholder text fits at every render size (peek clone!).
   Full-specificity selectors: the base .composed-card .cc-panel.your-space
   rules are (0,4,0) and silently beat the earlier (0,2,0) cqi clamps -
   these MUST outrank them or the fit system never applies. */
.composed-card .cc-panel.your-space .ys-label { font-size: clamp(7px, 9cqi, 22px); line-height: 1.25; }
.composed-card .cc-panel.your-space .ys-sub { font-size: clamp(6px, 5cqi, 12px); line-height: 1.3; }
.composed-card .cc-panel.your-space .ys-headline { line-height: 1.25; }
.your-space .ys-placeholder { gap: clamp(1px, 1cqi, 4px); min-height: 0; overflow: hidden; padding: 2%; }
.composed-card .cc-panel.your-space .ys-placeholder .chip { font-size: clamp(5px, 5cqi, 12px); padding: clamp(1px, 1cqi, 5px) clamp(2px, 2cqi, 10px); line-height: 1.2; }
/* MOBILE-TILE-01: the on-card counter scales with its slot (cqi), same
   dialect as the decision block counter at panel size */
.composed-card .cc-panel.your-space .ys-placeholder .pf-counter { font-size: clamp(5px, 5cqi, 12px); gap: clamp(2px, 1.4cqi, 7px); }
.composed-card .cc-panel.your-space .ys-placeholder .pf-counter .dk-segs { gap: clamp(1px, 0.6cqi, 3px); }
.composed-card .cc-panel.your-space .ys-placeholder .pf-counter .dk-segs i { width: clamp(3px, 1.9cqi, 9px); height: clamp(3px, 2.1cqi, 10px); border-width: clamp(0.75px, 0.45cqi, 2px); }
/* (h) hamburger right-aligned in the mobile header */
@media (max-width: 768px) {
  .nav-row { justify-content: space-between; }
  .nav-burger { margin-left: auto; order: 2; }
}

/* over-tier state: unmistakable, CTA visibly blocked */
.deck-pane.over-tier .price-lines { outline: 2px solid #C22C00; outline-offset: 4px; border-radius: 6px; }
.btn-disabled { opacity: .5; pointer-events: none; }


/* ============ MOBILE-COMPACT-01: portrait hero row (<768px) ============ */
.only-phone { display: none; }
@media (max-width: 767px) {
  /* the hero row REPLACES the sticky peek strip and the slim advisor strip
     on portrait phones; both keep their behaviour at 768 (tablet) */
  .peek-strip { display: none !important; }
  .only-phone:not([hidden]) { display: block; }
  /* fixed-height tiles: content scales inside, edges never move on selection */
  .hero-row:not([hidden]) { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-block: var(--stack-gap) 0; }
  /* the real sheet alone on the phone (Jon-ruled 2026-08-22): one tile, the artefact, when the map tile is empty */
  .hero-row.hero-row--one:not([hidden]) { grid-template-columns: 1fr; max-width: 300px; margin-inline: auto; }
  .hero-row.hero-row--one .hero-tile { aspect-ratio: 210 / 297; }   /* alone, the tile is the sheet's own shape */
  .hero-row .hero-tile[hidden] { display: none; }
  /* THE SPACE IS KEPT, THE PIXELS ARE NOT (Jon-ruled 2026-09-04). The row holds
     its box from the first frame so the page never changes shape under a press;
     visibility rather than display, so nothing is painted and nothing can be
     touched until its content is real. */
  .hero-row.hero-row--reserved { visibility: hidden; }
  .hero-tile {
    position: relative; aspect-ratio: 7 / 5; padding: 0; overflow: hidden; display: block;
    background: var(--pf-grey-50); border: 1px solid var(--pf-border); border-radius: var(--s-1);
    cursor: pointer;
  }
  /* MOBILE-TILE-01: .print-object (DESIGNER-02 physicality) sets
     position:relative LATER in the cascade at equal specificity — it beat
     this pin, the thumb sized to its CONTENT and the fit maths letterboxed
     the clone below the tile's visible window (blank tile). The doubled
     selector keeps the pin winning whatever classes the thumb carries. */
  .hero-tile .hero-thumb, .hero-tile .hero-map { position: absolute; inset: 0; display: block; }
  /* PARITY-SWEEP-01: the REAL map slot docks into the tile (one render path,
     mirrors the lightbox state at mini scale, updates live) */
  .hero-map .map-slot { height: 100%; margin: 0; border: 0; border-radius: 0; background: none; }
  .hero-map .map-slot .map-art, .hero-map .map-slot .zone-mini { height: 100%; }
  .hero-map .map-slot .zone-wrap, .hero-map .map-slot .map-wrap { border: 0; border-radius: 0; height: 100%; }
  .hero-map .map-slot .expand-btn { display: none; } /* the tile IS the expand control */
  /* MOBILE-CARD-PREVIEW-01 s3: the tile fits the WHOLE artefact — scaled,
     centred, letterboxed (JS sets the exact fit; this is the base state).
     No cropped masthead, no clipped text, uniform with the solo tile. */
  .hero-thumb > .peek-scale { width: 460px; transform-origin: 0 0; }
  .hero-thumb > .peek-scale .cc-flip { display: none; }
  .hero-thumb img.solo-mini { width: 100%; height: 100%; object-fit: contain; object-position: center; background: #F4F2EE; }
  /* MOBILE-TILE-01: the solo mini is a DRESSED paper object — same printed
     treatment the stage/sheet solo-frame carries, scaled to the tile */
  .hero-thumb .solo-mini-frame { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 7px; background: var(--pf-grey-50); }
  .hero-thumb .solo-mini-frame img.solo-mini {
    width: auto; height: auto; max-width: 100%; max-height: 100%; background: #fff;
    border-radius: 0; /* paper ruling */ border-bottom: 2px solid rgba(0, 0, 0, 0.12);
    box-shadow: 0 1px 2px rgba(24, 20, 14, 0.14), 0 5px 14px rgba(24, 20, 14, 0.2);
  }
  .hero-map > img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }
  .hero-map > img { object-position: center; }
  .hero-blob {
    position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1;
    border-radius: 50%; border: 2px dashed rgba(253, 54, 1, .85);
    background: radial-gradient(circle, rgba(253, 54, 1, .38), rgba(253, 54, 1, .12) 62%, transparent 72%);
  }
  .hero-plane { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -88%); z-index: 2; }
  .hero-expand {
    position: absolute; top: 0; right: 0; z-index: 3;
    width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  }
  /* advisor docks below the hero row as its own full-width expandable box */
  .mob-adv-slot { min-height: 46px; margin-block: var(--stack-gap) 0; }
  /* controls follow at the same token; step padding halves the token so the
     rhythm across each section separator is one token too */
  .deck-body { padding-block-start: var(--stack-gap); }
  .deck-pane .cfg-step { padding-block: calc(var(--stack-gap) / 2); }
  /* pricing summary line + CTA above the controls; fixed box, text changes */
}


/* ============ DESIGNER-02: mockup physicality + pro design ============ */
/* ONE printed-object treatment: paper edge, soft drop shadow, faint surface
   texture. Applied to: stage card faces, solo stage frame, designer preview,
   variant strip thumbs, mobile hero artefact tile, preview sheet (same
   nodes), lightbox frame. Restraint is the premium tell - no scenes. */
.print-object {
  position: relative;
  box-shadow: 0 1px 2px rgba(24, 20, 14, .12), 0 10px 26px rgba(24, 20, 14, .16);
  border-radius: 0; /* paper ruling: square page corners; the printed-object shadow/texture treatment survives */
}
.print-object::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .4), inset 0 0 0 2px rgba(24, 20, 14, .05);
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/><feComponentTransfer><feFuncA type="linear" slope="0.05"/></feComponentTransfer></filter><rect width="140" height="140" filter="url(%23n)"/></svg>');
  background-size: 140px 140px; mix-blend-mode: multiply;
}
.cc3d .cc-face { box-shadow: 0 30px 58px rgba(0, 0, 0, 0.24), 0 6px 16px rgba(0, 0, 0, 0.12); }
.dz-strip button.print-object::after { background-size: 70px 70px; }

/* print-truth caption: quiet, values live in Airtable Config (never code);
   the line reserves no space when the spec is absent (gated) */
.print-truth { position: absolute; right: 24px; bottom: 12px; margin: 0; font-size: 11px; color: var(--muted, #55524D); letter-spacing: .02em; }
.print-truth:empty { display: none; }
.artefact-stage [data-pv="designer"] .print-truth { position: static; text-align: center; height: 14px; font-size: 10px; }

/* pro-design tile: badge treatment, no thumbnail; strip slot sizing */
.dz-strip button.dz-pro {
  width: 116px; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; background: var(--pf-white); border: 1.5px dashed var(--pf-border);
  font-size: 10px; line-height: 1.25; color: var(--pf-black); padding: 4px 6px; text-align: center;
}
.dz-strip button.dz-pro .dz-pro-badge {
  font-weight: 800; font-size: 10px; color: var(--pf-brand);
  border: 1px solid currentColor; border-radius: var(--r-pill); padding: 1px 8px;
}
.dz-strip button.dz-pro[aria-pressed="true"] { border-style: solid; border-color: var(--pf-brand); }
.dz-strip button.dz-pro .dz-pro-sla { color: var(--muted, #55524D); font-size: 9px; }

/* card-tab pro line (reserved; text toggles) + comp strike-through */
.price-block .pro-line { display: block; margin: 0; min-height: 20px; }
.pro-line s, .pl s { color: var(--pf-grey-600); font-weight: 400; margin-right: 6px; }

/* lightbox inspection: zoomable content (existing 3x renders - no new asset) */
.lightbox-body.zoomable { overflow: hidden; cursor: zoom-in; touch-action: none; }
.lightbox-body.zoomable.zoomed { cursor: grab; }
.lightbox-body .lb-zoom { transform-origin: center center; transition: transform 120ms ease-out; will-change: transform; }
.lightbox-body .lb-frame { display: flex; }
/* CARD-PORTRAIT-01: fit-to-view applies to BOTH formats — the solo flyer's
   full-res render must open fully visible too */
.lightbox-body .lb-frame img { max-height: 88vh; max-width: 92vw; width: auto; height: auto; object-fit: contain; }


/* ============ LOCAL-PHASE-B: location chip + selector + /[city] page ============ */
/* B-FIX-02: ONE chip ELEMENT site-wide (the old desktop/mobile pair let the
   mobile variant leak onto desktop: .loc-chip's display beat .only-mobile's
   display:none at equal specificity). Desktop: second half of the brand
   lockup - logo first, chip second, one unit ("PromoFlyers ▸ Bristol").
   Mobile: pin + city left of the burger; the pin/arrow glyphs swap by
   viewport, the element never duplicates. */
.loc-chip {
  display: inline-flex; align-items: center; gap: 5px;
  border: 0; background: none; cursor: pointer; padding: 6px 8px;
  font: 700 14px/1 Inter, system-ui, sans-serif; color: var(--pf-black);
  /* .brand carries order:1 - the chip must join it or it renders BEFORE the
     logo (the root cause of the old chip sitting left of the lockup) */
  order: 1;
}
.loc-chip:hover, .loc-chip:focus-visible { color: var(--pf-brand); }
.loc-chip svg { color: var(--pf-brand); flex: none; }
.loc-chip .loc-pin { display: none; }
@media (min-width: 768px) {
  /* lockup: pull back half the row gap so logo->chip reads as one unit
     (one --s-1 token) while other header items keep the row's --s-2 */
  .loc-chip { margin-left: calc(-1 * var(--s-1)); }
}
@media (max-width: 767px) {
  .loc-chip { margin-left: auto; font-size: 13px; }
  .loc-chip .loc-pin { display: block; }
  .loc-chip .loc-arrow { display: none; }
  .nav-burger { margin-left: 8px !important; }
}

/* selector: bottom sheet on mobile, dropdown panel on desktop */
.loc-selector { border: 0; padding: 0; border-radius: var(--r-card); box-shadow: 0 12px 48px rgba(0,0,0,.18); width: min(420px, 92vw); }
.loc-selector::backdrop { background: rgba(26,26,26,.4); }
.loc-selector:not([open]) { display: none; }
.loc-selector .loc-body { padding: 18px 20px 16px; }
.loc-selector .close { position: absolute; top: 2px; right: 2px; }
.loc-h { font-size: 12px; letter-spacing: .06em; text-transform: uppercase; color: var(--pf-grey-600); margin: 12px 0 6px; }
.loc-list { list-style: none; margin: 0; padding: 0; }
.loc-list li { margin: 0; }
.loc-list a {
  display: flex; justify-content: space-between; gap: 12px; align-items: baseline;
  padding: 9px 10px; border-radius: 8px; text-decoration: none; color: var(--pf-black); font-weight: 600;
}
.loc-list a:hover, .loc-list a:focus-visible { background: var(--pf-orange-tint); }
.loc-list a .loc-sum { font-weight: 400; font-size: 12px; color: var(--pf-grey-600); }
.loc-foot { margin: 14px 0 0; }
#loc-search { width: 100%; padding: 9px 12px; border: 1px solid var(--pf-border); border-radius: 8px; margin-bottom: 4px; font: 400 15px/1.4 Inter, system-ui, sans-serif; }
@media (max-width: 767px) {
  .loc-selector { position: fixed; inset: auto 0 0 0; margin: 0; width: 100vw; max-width: 100vw; border-radius: var(--r-card) var(--r-card) 0 0; max-height: 78dvh; overflow: hidden; display: flex; flex-direction: column; }
  .loc-selector[open] { animation: pf-sheet-up 200ms var(--ease); }
  .loc-selector:not([open]) { display: none; }
  .loc-selector .loc-body { overflow-y: auto; }
  .loc-selector[open] { animation: pf-sheet-up 200ms var(--ease); }
}

/* LOCAL-UNIFY-01: the unified offers section reuses the offers-list item
   treatment from the retired standalone page */
.ct-offers ul, #ct-offers { list-style: none; margin: 8px 0 0; padding: 0; }
#ct-offers li { padding: 10px 0; border-bottom: 1px solid var(--pf-border); display: flex; gap: 10px; flex-wrap: wrap; align-items: baseline; }
#ct-offers li b { flex: none; }
#ct-offers li .cat { font-size: 12px; color: var(--pf-grey-600); }
/* ============ LOCAL-UNIFY-01: real card state on the deck preview ============
   The composed card (the deck's hero object) carries the ratified slot-state
   treatments (decisions-v2 s3) when real state exists for the selected area. */
.composed-card .cc-panel.lc-slot { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; text-align: center; padding: 8px; overflow: hidden; }
.composed-card .cc-panel.lc-slot img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* OWN-BRAND-MOVE fix: the tag SEATS flush in its reserved corner — the old
   6px-inset pill hovered over the artwork's own bottom copy and read as
   floating. Flush + top-left radius only; the panel's overflow clipping
   rounds the outer corner. Legible, minimal occlusion of ad copy. */
.composed-card .cc-panel .lc-tag {
  position: absolute; right: 0; bottom: 0; z-index: 2;
  font-size: 10px; font-weight: 700; letter-spacing: 0.04em; line-height: 1.2;
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid var(--pf-border); border-right: 0; border-bottom: 0;
  border-radius: 4px 0 0 0; padding: 2px 7px; color: var(--pf-grey-600);
}
/* SOLD without artwork: a proper sold panel - never a black schematic box */
.composed-card .lc-sold { background: linear-gradient(160deg, #FFFFFF, #F4F1EC); }
.composed-card .lc-sold .lc-cat { font-size: 11px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--pf-grey-600); }
.composed-card .lc-sold .lc-biz { font-weight: 800; font-size: 16px; line-height: 1.15; overflow-wrap: anywhere; }
.composed-card .lc-sold .lc-offer { font-size: 12.5px; color: var(--pf-grey-600); line-height: 1.3; }
/* RESERVED: HEAVY blur of a GENERIC drawn placeholder - never artwork */
.composed-card .lc-reserved { background: #EFEBE4; }
.composed-card .lc-reserved .lc-ghost { position: absolute; inset: 0; filter: blur(9px); transform: scale(1.1); }
.composed-card .lc-reserved .lc-ghost i { position: absolute; display: block; background: #C9C2B8; border-radius: 3px; }
.composed-card .lc-reserved .lc-ghost i:nth-child(1) { left: 10%; top: 14%; width: 62%; height: 16%; }
.composed-card .lc-reserved .lc-ghost i:nth-child(2) { left: 10%; top: 38%; width: 80%; height: 9%; background: #DAD4CA; }
.composed-card .lc-reserved .lc-ghost i:nth-child(3) { left: 10%; top: 52%; width: 70%; height: 9%; background: #DAD4CA; }
.composed-card .lc-reserved .lc-ghost i:nth-child(4) { left: 10%; top: 72%; width: 44%; height: 14%; background: var(--pf-brand); opacity: 0.55; }
.composed-card .lc-reserved .lc-chip {
  position: relative; z-index: 2; font-size: 11px; font-weight: 800; letter-spacing: 0.03em;
  background: var(--pf-white); border: 1px solid var(--pf-border); border-radius: var(--r-pill); padding: 4px 10px;
}
.composed-card .lc-reserved .lc-frame {
  position: absolute; left: 8px; bottom: 6px; z-index: 2;
  font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--pf-grey-600);
}
/* OWN-BRAND: real artwork (or the house wordmark), subtle corner tag */
.composed-card .lc-own { background: var(--pf-grey-50); }
.composed-card .lc-own .lc-own-house { font-weight: 900; font-size: 20px; letter-spacing: -0.01em; }
.composed-card .lc-own .lc-own-house b { color: var(--pf-brand); }
/* OPEN (beyond the customer's live slot): the invitation treatment, quieter */
.composed-card .lc-open-x .ys-label { font-size: 15px; }
/* CARD-FACE-02: open invitations on LIVE cards unify with the founding
   treatment — the face-system frame makes an open card read as opportunity.
   ys-static = the default (empty-state) face panels; same dress, no cursor */
.composed-card .cc-panel.ys-static { cursor: default; }
.composed-card .cc-panel.lc-open-x, .composed-card .cc-panel.ys-static {
  border: 1.5px solid var(--pf-orange); background: linear-gradient(160deg, #FFFFFF, var(--pf-orange-tint));
  box-shadow: inset 0 0 0 4px #fff, inset 0 0 0 5.5px rgba(253, 54, 1, 0.45);
}
/* own-brand ask-us affordance (displaceable — honest, quiet) */
.cc-panel.lc-own[data-askus] { cursor: pointer; }
.lc-askus {
  position: absolute; inset: auto 4px 4px 4px; z-index: 6;
  background: var(--pf-black); color: var(--pf-white); border-radius: 8px;
  padding: 8px 10px; font-size: clamp(8px, 5cqi, 12px); line-height: 1.35; text-align: left;
}
.lc-askus a { display: inline-block; margin-top: 4px; color: var(--pf-white); font-weight: 700; text-decoration: underline; }
.deck--founding .cc-panel.your-space:not(.lc-slot) { display: none; }
/* inline 8-slot segments inside the existing dk-avail-row (no new height) */
.dk-segs { display: inline-flex; gap: 3px; margin-right: 8px; vertical-align: middle; }
/* MOBILE-POLISH-06: empty = warm border + subtle warm fill (the old bare
   outline vanished on the warm background); taken stays the orange fill */
.dk-segs i { width: 9px; height: 10px; border-radius: 2px; border: 2px solid var(--pf-seg-border); background: var(--pf-orange-tint); display: block; }
.dk-segs i.sg-sold { background: var(--pf-brand); border-color: var(--pf-brand); }
.dk-segs i.sg-res { border-color: var(--pf-brand); background: repeating-linear-gradient(45deg, rgba(253, 54, 1, 0.55) 0 3px, rgba(253, 54, 1, 0.18) 3px 6px); }
/* arrival overlay: house modal, availability-first */
.wl-modal.arrive h3 { margin-bottom: 4px; }
.wl-modal.arrive .arr-sub { font-weight: 600; margin-bottom: var(--s-2); color: var(--pf-grey-600); }

/* disclosure popover: the deck availability component */
.ct-info-pop {
  position: absolute; z-index: 40; top: 100%; left: 0; margin-top: 6px;
  background: var(--pf-black); color: var(--pf-white); font-size: 13px; font-weight: 400;
  line-height: 1.45; padding: 10px 12px; border-radius: 8px; max-width: 320px;
}
/* deck disclosure row: present in BOTH the stacked chip context and the
   docked rail card (the price-block travels whole); reserved height */
.price-block .dk-avail-row { display: block; }
.price-block .dk-avail-row .info-i { display: inline-block; padding: 0 6px; }
/* MOBILE-POLISH-06: stacked layouts show ONE bar — the counter's own; the
   standalone #dk-segs serves only the rail (unhidden there, above) */
.price-block .fold-scarcity > .dk-segs { display: none; }
/* the ⓘ sits BESIDE the counter, never orphaned on its own line */
.price-block .fold-scarcity.dk-avail-row { display: flex; align-items: baseline; }
.price-block .fold-scarcity.dk-avail-row .pf-counter { min-width: 0; }

/* B-FIX-01 tab fallback (MEASURED: full set fits >=375; 360 was 17px over):
   below 375 the card label stays full, Solo / Designer take their short forms */
@media (max-width: 374px) {
  .deck-tabs [data-tab="solo"] .tab-full, .deck-tabs [data-tab="designer"] .tab-full { display: none; }
  .deck-tabs [data-tab="solo"] .tab-short, .deck-tabs [data-tab="designer"] .tab-short { display: inline; }
}
/* FACE-CALM-01: >=1600 the fluid 60fr stage overran the UNSCROLLED 900
   fold by ~30px — the card gets a fixed cap (fold headroom beats bulk) */
@media (min-width: 1600px) {
  .deck-preview [data-composed-card] { max-width: 480px; }
}
/* B-FIX-01 fold rebalance: the rail card's fact rows compress at >=1600 so
   the card group stays under the solo group (242) - D02's pro line + E's
   disclosure row had pushed it to 261 and the composition past the fold.
   LOOK-RESTORE-02 s1: the short-viewport tier docks the same rail — the
   compression and the rail-is-the-decision-block rules ride with it. */
@media (min-width: 1200px) {
  .pricing-slot .price-block { gap: 4px; }
  .pricing-slot .price-block .price-chip { padding: 4px 10px; font-size: 14px; }
  .pricing-slot .price-block .scarcity-line,
  .pricing-slot .price-block .pro-line { min-height: 18px; line-height: 18px; font-size: 12px; }
  /* at rail width the pricing card IS the decision block: chip = price,
     spaces = availability, the closes row carries the drop-week/(estimated)
     fact - the single-line block is the sub-rail presentation */
  .pricing-slot .decision-line { display: none; }
  .pricing-slot #dk-area-districts { display: none; }
  .pricing-slot .dk-avail-row { min-height: 18px !important; height: 18px; margin: 0 !important; }
  .pricing-slot .dk-avail-row .info-i { padding: 2px 4px; } /* 14px icon + 2px = the row's 18px */
}
/* FACE-CALM-01: <=820px-height tier PRICING compression — placed AFTER the
   rail-compression block above so the tier constants win the cascade. The
   rail floor is the pricing card reserved min-content; these rows clear
   the UNSCROLLED 768 fold. */
@media (min-width: 1200px) and (max-width: 1599px) and (max-height: 820px) {
  .pricing-slot { padding: 8px 14px; }
  .pricing-slot .price-lines .pl { min-height: 22px; }
  .pricing-slot .price-block .price-chip { padding: 3px 9px; font-size: 13px; }
  .pricing-slot .price-block .scarcity-line,
  .pricing-slot .price-block .pro-line { min-height: 15px; line-height: 15px; font-size: 11px; }
  .pricing-slot .rp-dz-status, .pricing-slot .rp-dz-ctx { min-height: 15px; }
  .pricing-slot .dk-avail-row { min-height: 15px !important; height: 15px; }
}


/* S3b MODE: the build-site marker (renders only where site.js flags
   orders-off — never on customer origins) */
.build-ribbon {
  background: var(--pf-black);
  color: var(--pf-white);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.4px;
  text-align: center;
  padding: 6px var(--gutter);
}
.build-ribbon::before { content: '\25CF'; color: var(--pf-orange); margin-right: 8px; }

/* S3b B1 — IA LAW: master region row + tool-link Design tab + coming-soon */
.deck-region {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  /* headroom so the select's focus/highlight ring renders in full — the
     first thing on the page must never look clipped (Jon, 2026-08-01) */
  padding-top: 6px;
  margin-bottom: var(--s-2, 16px);
}
.deck-city { display: flex; align-items: baseline; gap: 10px; }
.dc-label {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--pf-grey-600, #6B6B6B);
}
.dc-city {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 15px;
  font-weight: 800;
  color: var(--pf-black);
}
.dc-city svg { color: var(--pf-orange); }
.deck-region select { outline-offset: 2px; }
.deck-region label {
  font-size: 13px;
  font-weight: 700;
  color: var(--pf-grey-600, #6B6B6B);
  text-transform: uppercase;
  letter-spacing: 0.4px;
}
.deck-region select {
  font: inherit;
  font-weight: 700;
  padding: 8px 12px;
  border: 1px solid var(--pf-border);
  border-radius: var(--r-btn);
  background: var(--pf-white);
  max-width: 320px;
}
.deck-tabs .tab-tool {
  margin-left: auto;
  border: 1px solid var(--pf-orange);
  color: var(--pf-orange);
  border-radius: var(--r-btn); /* stale pill declaration cleaned (one-corner law) */
}
.tab-soon {
  font-size: 11px;
  font-weight: 700;
  color: var(--pf-grey-600, #6B6B6B);
  border: 1px dashed var(--pf-border);
  border-radius: var(--r-pill);
  padding: 4px 10px;
  white-space: nowrap;
  align-self: center;
}
@media (max-width: 700px) { .tab-soon { display: none; } }

/* S3b B1 — the 32-cell sheet artefact (deck-integration §1). While the
   sheet stands, the V1 composed card + its 8-dialect counter hide —
   one surface, one dialect. */
.stage-content.sheet-live [data-composed-card] { display: none; }
.stage-content.sheet-live #dk-card-remain { display: none; }
.pfsh-sheet {
  aspect-ratio: 210 / 297;
  background: var(--pf-white);
  border: 1px solid var(--pf-border);
  border-radius: 0; /* paper ruling */
  box-shadow: var(--shadow-hover);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.pfsh-band {
  background: var(--pf-black);
  color: var(--pf-white);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 4%;
  height: 6.7%;
  font-size: clamp(9px, 1.4vw, 14px);
  font-weight: 700;
}
.pfsh-band .pfsh-brand b { color: var(--pf-orange); }
.pfsh-band.pfsh-foot { height: 4%; font-size: clamp(7px, 1vw, 11px); font-weight: 600; color: #ccc; }
.pfsh-count { color: var(--pf-orange); }
.pfsh-grid {
  flex: 1;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(4, 1fr);
  gap: 1.9%;
  padding: 3.8%;
}
.pfsh-cell {
  border: 1px solid #e2ded7;
  border-radius: 6px;
  background: #faf8f5;
  overflow: hidden;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 3px;
  text-decoration: none;
}
.pfsh-art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.pfsh-bn { font-weight: 800; font-size: clamp(7px, 1.1vw, 12px); line-height: 1.2; color: var(--pf-black); }
.pfsh-cat { font-size: clamp(6px, 0.9vw, 10px); color: var(--pf-grey-600, #6B6B6B); }
.pfsh-own { position: absolute; top: 3px; right: 4px; font-size: clamp(6px, 0.8vw, 9px); font-weight: 700; color: var(--pf-grey-400, #9A9A9A); cursor: help; }
.pfsh-free { border-style: dashed; border-color: #d8cfc2; background: #fffdf9; }
.pfsh-free .pfsh-bn { color: var(--pf-orange); font-size: clamp(6px, 1vw, 10px); }
.pfsh-community {
  background: #f4f1ec;
  border-top: 1px solid var(--pf-border);
  font-size: clamp(7px, 1vw, 11px);
  color: var(--pf-grey-600, #6B6B6B);
  text-align: center;
  padding: 1.2% 4%;
}
.pfsh-nav { display: flex; gap: 8px; margin-top: 8px; }
.pfsh-fbtn {
  flex: 1;
  padding: 8px;
  border-radius: var(--r-btn);
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  border: 2px solid var(--pf-border);
  background: var(--pf-white);
  cursor: pointer;
}
.pfsh-fbtn.is-sel { border-color: var(--pf-orange); color: var(--pf-orange); }

/* S3b B1 — the IN-PAGE booking rail (same-page commerce + separation laws).
   Selection block and details block are SEPARATE cards, never interleaved.
   While this rail stands, the V1 pricing block (8-dialect) stands down. */
.pfsh-rail-hidden { display: none !important; }
/* SOLD OUT, AND IT LEADS SOMEWHERE (Jon-ruled 2026-08-16): "Currently at
   capacity — join the waitlist." The line sits under the shapes it explains,
   and carries the area's own interest capture rather than a second one. */
.pfsh-full {
  margin: 8px 0 0; padding: 10px 12px;
  background: var(--pf-grey-50); border: 1px solid var(--pf-border); border-radius: 8px;
  font: 600 13px/1.45 Inter, system-ui, sans-serif; color: var(--pf-black);
}
.pfsh-full[hidden] { display: none; }
.pfsh-full .pf-int { display: inline; }
/* THE WAITLIST IS THE NEXT STEP, SO IT IS A TARGET A THUMB CAN HIT. Measured
   on the full-edition fixture at 390: the link was 156x16 — the one thing left
   to press on that surface, at a third of the finger floor. */
.pfsh-full .pf-int-go {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: 0 4px; font-weight: 700; text-decoration: underline;
}
#pfsh-rail { display: flex; flex-direction: column; gap: 12px; }
#pfsh-rail.pfsh-flash .pfsh-railcard:first-child { outline: 2px solid var(--pf-orange); transition: outline-color 1.2s ease 0.6s; outline-color: transparent; }
.pfsh-railcard {
  background: var(--pf-white);
  border: 1px solid var(--pf-border);
  border-radius: var(--r-card);
  padding: 14px;
}
.pfsh-railcard h3, .pfsh-railcard h4 {
  font-size: 11px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--pf-grey-600, #6B6B6B);
  margin: 0 0 8px;
}
.pfsh-railcard h3 + h3, .pfsh-railcard h4 + h4, .pfsh-terms + h3, .pfsh-terms + h4 { margin-top: 12px; }
.pfsh-coming {
  font-size: 13px;
  font-weight: 600;
  color: var(--pf-grey-600, #6B6B6B);
  background: var(--pf-grey-50, #F9F9F9);
  border: 1px solid var(--pf-border);
  border-radius: 10px;
  padding: 9px 11px;
  margin: 0 0 10px;
}
/* THE SPACE SELECTOR CANNOT BURST ITS BOX (Jon's walk, 2026-08-10):
   picking Quad pushed the content out of the container at 390. Three or
   four shape cards on a 308px column cannot each keep an intrinsic width,
   so they SHRINK and WRAP, and every label inside them is allowed to be
   narrower than its longest word. */
.pfsh-shapes { display: flex; flex-wrap: wrap; gap: 6px; }
.pfsh-shape {
  flex: 1 1 84px; min-width: 0; max-width: 100%; overflow-wrap: anywhere;
  border: 2px solid var(--pf-border);
  border-radius: 10px;
  background: var(--pf-white);
  font: inherit;
  padding: 8px 6px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: center;
}
.pfsh-shape.is-sel { border-color: var(--pf-orange); }
.pfsh-shape:disabled { opacity: 0.45; cursor: default; }
.pfsh-st { font-weight: 800; font-size: 13px; min-width: 0; overflow-wrap: anywhere; }
.pfsh-sp, .pfsh-tp { font-size: 11px; font-weight: 700; color: var(--pf-grey-600, #6B6B6B); white-space: nowrap; }  /* "£49.95/mont h" wrapped mid-word at 390 (walk, 2026-08-22) */
.pfsh-terms { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.pfsh-term {
  border: 2px solid var(--pf-border);
  border-radius: 10px;
  background: var(--pf-white);
  font: inherit;
  padding: 9px 11px;
  cursor: pointer;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.pfsh-term.is-sel { border-color: var(--pf-orange); background: var(--pf-orange-tint); }
.pfsh-term.pfsh-hero { border-color: rgba(253, 54, 1, 0.4); }
.pfsh-term.pfsh-hero .pfsh-tt { font-size: 15px; }
.pfsh-term.pfsh-hero .pfsh-tp { color: var(--pf-orange); }
.pfsh-lead {
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--pf-orange);
}
.pfsh-tt { font-weight: 800; font-size: 13px; }
.pfsh-td { font-size: 11px; color: var(--pf-grey-600, #6B6B6B); }
.pfsh-in {
  width: 100%;
  font: inherit;
  font-size: 14px;
  padding: 10px 11px;
  border: 1px solid var(--pf-border);
  border-radius: 10px;
  margin-bottom: 8px;
  background: var(--pf-white);
}
/* OFFER FIELDS NEVER CLIP: one visual line that grows with the words —
   never a scrollbar, never a resize grip, never hidden copy */
textarea.pf-offer-ta {
  resize: none;
  overflow: hidden;
  min-height: 0;
  line-height: 1.35;
  display: block;
}
.pfsh-cta {
  display: block;
  width: 100%;
  padding: 12px;
  border: 0;
  border-radius: var(--r-btn);
  background: var(--pf-orange);
  color: var(--pf-white);
  font: inherit;
  font-size: 14px;
  font-weight: 800;
  cursor: pointer;
}
.pfsh-cta:disabled { opacity: 0.55; cursor: default; }
.pfsh-note { font-size: 11px; color: var(--pf-grey-400, #9A9A9A); margin: 8px 0 0; }

/* DECK LAYOUT LAW: left = selection (V1 steps give way for sheet areas);
   centre = the artefact fitting the stage perfectly (no gaps); right =
   context + the live order summary. */
.deck-pane.pfsh-left-live .cfg-step { display: none; }
#pfsh-rail { margin-bottom: 12px; }
.stage-content.sheet-live:not([hidden]) { display: flex; flex-direction: column; }
.stage-content.sheet-live[hidden] { display: none !important; } /* hidden must WIN — the flex override once kept the card stage in the crossfade stack and shoved solo down 21px */
.stage-content.sheet-live .pfsh { flex: 1; display: flex; flex-direction: column; margin: 0; }
.pfsh-sheet { flex: 1; width: 100%; }
/* the sheet-selection summary renders ONLY on the Publications tab — on
   Flyers/Design it would both mislead (sheet copy on a solo surface) and
   grow the pricing-slot, displacing the stage rhythm (probe-isolated).
   B3's cart makes the right-column summary cross-product. */
.deck:not([data-deck="card"]) #pfsh-summary { display: none; }
#pfsh-summary .pfsh-sumpub { font-weight: 800; font-size: 13px; margin: 0 0 3px; }
#pfsh-summary .pfsh-sumline { font-weight: 800; font-size: 14px; margin: 0 0 2px; }
#pfsh-summary .pfsh-sumprice { font-weight: 700; font-size: 13px; color: var(--pf-orange); margin: 0; }
/* pubs list (left zone, below the rail — more clicks by design) */
#pfsh-pubs h3, #pfsh-pubs h4 { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.4px; color: var(--pf-grey-600, #6B6B6B); margin: 14px 0 6px; }
.pfsh-pub { border: 1px solid var(--pf-border); border-radius: 10px; background: var(--pf-white); margin-bottom: 6px; overflow: hidden; }
.pfsh-pubrow { display: flex; justify-content: space-between; align-items: center; gap: 8px; width: 100%; font: inherit; background: none; border: 0; padding: 10px 12px; cursor: pointer; }
.pfsh-pubname { font-weight: 700; font-size: 13px; }
.pfsh-pubmeta { display: flex; align-items: center; gap: 6px; color: var(--pf-grey-600, #6B6B6B); font-size: 11px; }
.pfsh-ic { display: inline-block; vertical-align: middle; }
.pfsh-reach { font-size: 10px; font-weight: 700; }
.pfsh-pubdetail { padding: 0 12px 12px; }
.pfsh-pubdetail img { max-width: 100%; border: 1px solid var(--pf-border); border-radius: 8px; }
.pfsh-pubtag { font-size: 12px; font-weight: 600; margin: 8px 0 0; }
.pfsh-pubgets { font-size: 12px; color: var(--pf-grey-600, #6B6B6B); margin: 4px 0 0; }
.pfsh-pubprice { font-size: 13px; font-weight: 800; color: var(--pf-orange); margin: 6px 0; }
.pfsh-pubcta { display: block; width: 100%; padding: 9px; border: 0; border-radius: 10px; background: var(--pf-orange); color: var(--pf-white); font: inherit; font-size: 12px; font-weight: 800; cursor: pointer; }
.pfsh-pubcta:disabled { opacity: 0.55; cursor: default; }

/* ARTEFACT-AT-EVERY-BREAKPOINT + MOBILE LAYOUT rulings: phones keep V1's
   ratified mobile system EXACTLY (hero tiles, map shrinking, order,
   spacing) — the sheet substitutes INTO it as the artefact. The tile's
   V1 card clone hides; the scaled sheet clone paints in its place. */
html.pfsh-live #dk-hero-thumb > * { display: none; }
html.pfsh-live #dk-hero-thumb .pfsh-tileclone { display: block; }
.pfsh-tileclone { position: absolute; inset: 0; overflow: hidden; }
.pfsh-tileclone .pfsh-sheet { transform-origin: top left; box-shadow: none; border-radius: 0; /* paper ruling */ }
/* masthead counter must clear the stage's expand/eye overlay buttons */
.pfsh-sheet .pfsh-band:first-child { padding-right: 56px; }
/* SELECTION-COLUMN ORDER law: publication selector + mini info hero */
.pfsh-pubsel {
  width: 100%;
  font: inherit;
  font-weight: 700;
  font-size: 13px;
  padding: 9px 11px;
  border: 1px solid var(--pf-border);
  border-radius: 10px;
  background: var(--pf-white);
}
.pfsh-minihero { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--pf-grey-600, #6B6B6B); margin-top: 8px; }
.pfsh-mh-cad { font-weight: 700; }
.pfsh-tp-mode > .pfsh-railcard:not(:first-child):not(.pfsh-tpcard) { display: none; }
.pfsh-tpstage { padding: 20px; text-align: center; }
.pfsh-tpstage img { max-width: 100%; max-height: 70%; border: 1px solid var(--pf-border); border-radius: 0; /* paper ruling */ }
.pfsh-tpstage h3 { font-size: 18px; font-weight: 800; margin: 10px 0 0; }
.pfsh-tpnote { font-size: 12px; color: var(--pf-grey-400, #9A9A9A); margin: 6px 0 0; }
/* DISTRIBUTION TAG SYSTEM: category icons in the tab row */
.deck-tabs .tab-dist { vertical-align: -1px; margin-right: 5px; opacity: 0.75; }

/* NAV GROUPING RULING: three blocks, one visual language — a colour wash
   and icon per group, nothing heavier. Tabs behave exactly as before. */
/* LOCATION+NAV BAND — FINAL FORM (2026-08-01, supersedes the card block):
   a full-width sub-header strip, header-like structure, its own background,
   EVERYTHING SQUARED — architecture, not ornament. Elements sit in the
   container, not on rules; the strip + pinned Design keeps every width
   collision-free. */
.loc-band {
  background: var(--pf-grey-50, #F9F9F9);
  border-bottom: 1px solid var(--pf-border);
}
/* v2 (2026-08-01): ONE low-key line, TWO dropdowns sharing one type
   system, glistening Design beside them — no squared blocks, no pill row.
   v3 addition: BASELINE alignment — the location reads as the first
   element of the same visual row as the strip items, never floating. */
.lb-line { display: flex; align-items: baseline; gap: 18px; padding-top: 9px; padding-bottom: 9px; }
.loc-band .lb-ddbtn {
  padding-top: 8px !important;
  padding-bottom: 8px !important;
  border-bottom: 3px solid transparent !important; /* matches the strip items' box — one row, level */
}
.loc-band .tab-tool { align-self: center; }
.loc-band .deck-tabs, .lb-line.deck-tabs { border-bottom: 0; overflow: visible; }
.lb-dd { position: relative; flex: none; } /* NEVER shrinks — shrink was the overlap/spill bug (v3 responsive ruling) */
.lb-pin { display: none; color: var(--pf-orange); }
/* mid widths: location compresses to pin + city, one line holds */
@media (min-width: 601px) and (max-width: 940px) {
  .lb-ln { display: none; }
  .lb-pin { display: inline; }
}
.lb-ddbtn {
  background: none;
  border: 0;
  cursor: pointer;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  color: var(--pf-black);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  padding: 4px 0;
}
.lb-ddbtn b, .lb-ddbtn [data-cat-current] { font-weight: 800; }
/* the band's dropdown + menu buttons are NOT tabs — V1's .deck-tabs button
   underline/padding must not leak into them */
.loc-band .lb-ddbtn,
.loc-band .lb-menu button {
  border: 0 !important;
  border-bottom: 0 !important;
  padding-bottom: 4px;
}
.loc-band .lb-menu button { padding: 7px 10px; }
.lb-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 60;
  min-width: 280px;
  max-width: min(92vw, 360px);
  max-height: 60vh;
  overflow-y: auto;
  background: var(--pf-white);
  border: 1px solid var(--pf-border);
  border-radius: 10px;
  box-shadow: var(--shadow-hover), 0 12px 32px rgba(0, 0, 0, 0.10);
  padding: 6px;
}
.lb-head {
  font-size: 13px;
  font-weight: 800;
  color: var(--pf-black);
  padding: 8px 10px 4px;
}
.lb-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: 13.5px;
  font-weight: 400;
  color: var(--pf-black);
  background: none;
  border: 0;
  border-radius: 8px;
  padding: 7px 10px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
button.lb-item:hover { background: var(--pf-grey-50, #F9F9F9); }
.lb-item.is-sel { color: var(--pf-orange); font-weight: 700; }
.lb-item .lb-ic { flex: none; color: var(--pf-grey-600, #6B6B6B); }
.lb-item.is-sel .lb-ic { color: var(--pf-orange); }
.lb-soon { color: var(--pf-grey-400, #9A9A9A); cursor: default; }
.loc-band .tab-tool { margin-left: auto; flex: none; }
/* v3: the visible product strip (Airbnb pattern — Baymard: hidden
   catalogues test badly; NN/g hybrid: visible + scroll). Live entries are
   REAL tabs (V1 underline = active state); soons muted, advertising the
   roadmap. */
.lb-strip { gap: 2px; }
.lbs-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: inherit;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--pf-grey-600, #6B6B6B);
  background: none;
  border: 0 !important;
  border-bottom: 3px solid transparent !important;
  padding: 8px 10px !important;
  white-space: nowrap;
  cursor: pointer;
  flex: none;
}
button.lbs-item:hover { color: var(--pf-black); }
.lbs-item[aria-selected="true"] { color: var(--pf-black); border-bottom-color: var(--pf-orange) !important; }
.lbs-item .lb-ic { flex: none; }
.lbs-soon { color: var(--pf-grey-400, #9A9A9A); cursor: default; font-weight: 600; }
.lbs-tag {
  font-style: normal;
  font-size: 9px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--pf-grey-400, #9A9A9A);
  border: 1px solid var(--pf-border);
  border-radius: 4px; /* house corner treatment — the round bubble was off-brand */
  padding: 1px 6px;
}
/* the ONE rotating coming-soon slot: ROLLS ticker-style — the current item
   slides gently out as the next slides in; no flash, no blink, barely
   noticeable — the slot quietly breathing through the roadmap. Sized a
   tad smaller than the product entries: the quietest element on the row,
   and its size says so. */
#lbs-soon { min-width: 132px; font-size: 12px; font-weight: 600; padding: 8px 6px !important; }
#lbs-soon .lb-ic { width: 12px; height: 12px; }
#lbs-soon .lbs-tag { font-size: 8px; padding: 1px 5px; }
/* location hierarchy typography: the eye lands on the LEVELS (UK, the
   cities — bigger, bold) and settles into a region as the quiet choice
   within them (smaller, understated) */
.lb-item.lb-lvl0 { font-weight: 800; font-size: 15px; }
.lb-item.lb-lvl1 { font-weight: 800; font-size: 15px; }
.lb-item.lb-lvl2 {
  padding-left: 24px;
  font-size: 12.5px;
  font-weight: 400;
  color: var(--pf-grey-600, #6B6B6B);
}
.lb-item.lb-lvl2.is-sel { color: var(--pf-orange); font-weight: 700; }
#lbs-soon .lbs-win { display: block; height: 20px; overflow: hidden; }
#lbs-soon .lbs-track { display: block; transition: transform 1.1s cubic-bezier(0.45, 0, 0.15, 1); }
#lbs-soon .lbs-track.is-rolling { transform: translateY(-20px); }
#lbs-soon .lbs-row { display: flex; align-items: center; gap: 6px; height: 20px; white-space: nowrap; }
@media (max-width: 600px) {
  /* ONE LOW-KEY LINE, ON THE PHONE TOO (Jon-ruled 2026-08-16, SUPERSEDES the
     approved two-row compact recorded here since B3.5). The IA law always said
     the band is "ONE low-key line with TWO dropdowns"; the phone had been the
     exception, and the second row cost 29px at the top of every handset —
     which is exactly the overflow the DESIGN section could not close by any
     means of its own, because its own floor is a ratified number.
     The parts are unchanged and nothing is dropped: the location dropdown
     never shrinks, the product strip takes the lane that is left and scrolls
     inside it as it always has, and the tool link stays pinned right. */
  .loc-band .lb-line { gap: 8px; flex-wrap: nowrap; align-items: center; }
  .lb-ddbtn { font-size: 13px; }
  #lb-loc { flex: 0 0 auto; }     /* never shrinks — text must not spill under Design */
  .lb-strip {
    order: 2; flex: 1 1 auto; min-width: 0;
    overflow-x: auto; -webkit-overflow-scrolling: touch;
  }
  .lbs-item { font-size: 12.5px; padding: 7px 8px !important; }
  .loc-band .tab-tool { order: 3; flex: 0 0 auto; margin-left: 0; padding: 6px 11px; font-size: 13px; }
}
.lb-city {
  background: none;
  border: 0;
  cursor: pointer;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  color: var(--pf-black);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  padding: 0;
}
.lb-city b { font-weight: 800; }
.lb-tap { color: var(--pf-grey-400, #9A9A9A); } /* furniture, not action — orange reserved for actions */
.loc-band .deck-tabs { border-bottom: 0; flex: 1 1 auto; min-width: 0; display: flex; align-items: center; overflow: visible; }
.loc-band .tab-group,
/* (squared-band remnant, superseded: FINAL FORM v2 + Jon's one-corner-token
   ruling 2026-08-02 — Design + B2B take the house 10px token; only the
   legacy area chips stay square) */
.loc-band .tg-badge,
.lb-area { border-radius: 0 !important; }
.lb-panel { background: var(--pf-white); border-bottom: 1px solid var(--pf-border); }
.lb-panel .container { display: flex; flex-wrap: wrap; gap: 8px; padding-top: 10px; padding-bottom: 12px; }
.lb-area {
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  padding: 8px 12px;
  border: 1px solid var(--pf-border);
  background: var(--pf-white);
  cursor: pointer;
}
.lb-area.is-sel { border-color: var(--pf-orange); color: var(--pf-orange); }
@media (max-width: 860px) {
  .lb-line { flex-wrap: wrap; row-gap: 8px; }
  .lb-city { flex: 1 1 100%; }
  .loc-band .deck-tabs { flex: 1 1 100%; }
}

/* (retired card-block styles kept harmless below) */
.loc-block {
  background: var(--pf-white);
  border: 1px solid var(--pf-border);
  border-radius: var(--r-card);
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: var(--s-3, 24px);
}
/* INTEGRATION STANDARD (2026-08-01): one component, one designer, one day.
   Uniform 10px row rhythm; house type scale only (15px/700-800 controls,
   11px caps whisper label); inner controls share the 10px corner, badges 8,
   the block itself the 16px card — the house nesting scale. */
.loc-block .deck-region { margin-bottom: 0; padding-top: 0; gap: 10px; }
.loc-block .deck-tabs { border-bottom: 0; padding-bottom: 0; }
.deck-city { white-space: nowrap; flex-wrap: nowrap; } /* ONE line, never wraps */
.deck-region select {
  font-size: 15px; /* was body 17px — a size jump against its neighbours */
  border-radius: 10px; /* matches the group pills, not the outer card */
}
.dc-city { background: none; border: 0; cursor: pointer; font: inherit; font-size: 15px; font-weight: 800; padding: 0; }
.dc-chev { font-size: 11px; color: var(--pf-grey-400, #9A9A9A); margin-left: 2px; }
/* COLLISION-FREE AT EVERY WIDTH: the scroll strip + pinned Design is the
   UNIVERSAL layout — when space runs out at any width, categories hand off
   to the approved scroll before anything can overlap. */
.tab-scroll {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  /* the hand-off edge breathes: clipped content fades out before the
     pinned Design — never a hard chop against the button */
  padding-right: 4px;
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 22px), transparent);
  mask-image: linear-gradient(90deg, #000 calc(100% - 22px), transparent);
}
.tab-scroll::-webkit-scrollbar { display: none; }
.deck-tabs .tab-tool { margin-left: 10px; }
.deck-tabs { overflow: visible; align-items: center; }
.deck-tabs .tab-tool { flex: none; align-self: center; }
/* NAV FINAL FORM: one compact line per group — solid black icon badge left
   (matches the active-tab underline black), break area, options right. */
.tab-group {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 4px 12px 4px 4px;
  border-radius: 10px;
}
/* BLEND + FLUSH RULINGS (2026-08-01): the icon box IS the leading segment
   of the pill — same colour as the outline, sharing its left edge and
   corners (overflow clips it to the pill's own radius). Never a chip. */
.tg-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: stretch;
  width: 30px;
  flex: none;
  color: var(--pf-white);
}
.tab-group { overflow: hidden; padding: 0 10px 0 0; }
/* NAV HEIGHT ruling (2026-08-01): trim the boxes — tighter row under the
   region selector, slightly smaller text, no empty headroom */
.deck-tabs .tab-group [role="tab"] { padding-top: 7px; padding-bottom: 7px; font-size: 15px; }
.tg-soon-opt { padding: 7px 0; font-size: 11px; }
.tab-group-home {
  background: var(--pf-orange-tint);
  border: 1px solid var(--pf-orange);
}
.tab-group-home .tg-badge { background: var(--pf-orange); }
.tab-group-soon {
  background: var(--pf-grey-50, #F9F9F9);
  border: 1px solid var(--pf-grey-400, #9A9A9A);
}
.tab-group-soon .tg-badge { background: var(--pf-grey-400, #9A9A9A); }
.tg-soon-opt { padding: 9px 0; }
.tg-soon-opt {
  font-size: 12px;
  font-weight: 700;
  color: var(--pf-grey-600, #6B6B6B);
  white-space: nowrap;
}
/* MOBILE (mirror-law addendum, 2026-08-01 refinement): FULL groups compact
   — same structure as desktop, text kept, ready for inventory later. Bare
   icon chips are rejected. */
@media (max-width: 860px) {
  /* MOBILE NAV (deliberate compact form; horizontal scroll APPROVED): full
     groups with their text, phone-sized, ONE tight swipeable row */
  .deck-tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; align-items: center; }
  .deck-tabs::-webkit-scrollbar { display: none; }
  .tab-group { gap: 5px; padding-right: 8px; flex: none; }
  .tg-badge { width: 22px; }
  .deck-tabs .tab-group [role="tab"] { padding-top: 6px; padding-bottom: 6px; font-size: 13px; }
  .tg-soon-opt { font-size: 10px; padding: 6px 0; }
  /* exactly ONE label per tab — the short one (kills the double render) */
  .tab-group [role="tab"] .tab-full { display: none; }
  .tab-group [role="tab"] .tab-short { display: inline; }
  /* the Design button: ALWAYS VISIBLE at first paint — it lives OUTSIDE
     the scroll strip (.tab-scroll), pinned at the row's right edge;
     categories scroll past beside it. Vertically centred to the groups
     (V1's bottom-heavy underline padding overridden symmetric). */
  .tab-scroll { gap: 5px; }
  .deck-tabs .tab-tool {
    padding: 7px 12px !important;
    font-size: 13px;
    margin-left: 8px;
    border-bottom-width: 2px !important;
  }
  /* ADVERTISING IN: fits the viewport, compact, nothing clipped */
  .deck-region { gap: 8px; padding-top: 4px; }
  .deck-region label { font-size: 10px; letter-spacing: 0.3px; white-space: nowrap; }
  .deck-region select { flex: 1 1 auto; min-width: 0; max-width: none; width: auto; font-size: 14px; padding: 7px 10px; text-overflow: ellipsis; }
}

/* V1-DIALECT STAND-DOWN (mobile-mirror ruling): where the sheet stands,
   every V1-dialect element stands down — the advisor's card-price
   pre-engagement strip/panel, the mobile advisor slot, and the "Your
   card" peek label surface. V1 areas keep all of it untouched. */
/* INTEGRITY (Jon's find 2026-08-03): where the sheet stands, the KILLED V1
   composed card renders NOWHERE — not in the stage, not in a dialog, not in
   a clone. Structural, so no surface can leak it by escaping a scoped rule
   (the phone dialog moves the preview OUT of .deck, which is exactly how it
   leaked). V1 areas never carry pfsh-live, so their card is untouched. */
html.pfsh-live [data-composed-card] { display: none !important; }
/* THE ARTEFACT OWNS ITS HEIGHT on the Flyers tab (reopen addendum): the
   stage neither stretches to the deck grid's leftover column height nor
   centres the pane into floating offsets — the top edge is the shared
   --stage-top edge and the bottom edge is the artefact's plus the caption
   band. (The card tab keeps the stretch: the portrait sheet fills it.) */
.deck[data-deck="solo"] .deck-preview .artefact-stage { align-self: start; justify-content: flex-start; }
.artefact-stage > .stage-content {
  padding-top: var(--stage-top) !important;
  margin-top: 0 !important;
}
.artefact-stage > .stage-content > :first-child { margin-top: 0 !important; }
/* the sheet takes the HOUSE artefact treatment (same tilt + shadow as the
   composed card and solo frame) — same treatment, same projected top edge */
.stage-content.sheet-live .pfsh-sheet {
  transform: perspective(1600px) rotateX(1.4deg);
  transform-origin: 50% 60%;
  box-shadow: 0 30px 58px rgba(0, 0, 0, 0.24), 0 6px 16px rgba(0, 0, 0, 0.12);
}
/* the tile clone and the expand overlay stay flat (scaled/full views) */
.pfsh-tileclone .pfsh-sheet, #pfsh-lbx .pfsh-sheet { transform: none; box-shadow: none; }
#pfsh-lbx .pfsh-sheet { box-shadow: 0 22px 48px rgba(0, 0, 0, 0.4); }
.pfsh-tileclone .pfsh-sheet { transform-origin: top left; }

/* the expand overlay: the CURRENT artefact, full-size, any viewport */
#pfsh-lbx {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: rgba(0, 0, 0, 0.82);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 18px;
}
#pfsh-lbx .pfsh-sheet {
  width: min(92vw, calc(76vh * 210 / 297));
  max-width: 100%;
  flex: none;
  align-self: center;
  margin: 0 auto;
}
#pfsh-lbx .pfsh-nav { width: min(92vw, calc(76vh * 210 / 297)); margin: 0; }
#pfsh-lbx .pfsh-fbtn { background: var(--pf-white); }
.pfsh-lbx-x {
  position: absolute;
  top: 14px;
  right: 16px;
  width: 36px;
  height: 36px;
  border: 0;
  border-radius: 10px;
  background: var(--pf-white);
  font: inherit;
  font-size: 16px;
  font-weight: 800;
  cursor: pointer;
}
/* STYLE RULING: the Design tool-link matches the ratified "AI design it"
   primary — solid brand, white, weight 800, house radius, shimmer ×3 */
.deck-tabs .tab-tool {
  margin-left: auto;
  background: var(--pf-brand);
  border: 2px solid var(--pf-brand);
  color: var(--pf-white);
  border-radius: 10px;
  font-weight: 800;
  padding: 8px 16px;
  position: relative;
  overflow: hidden;
}
.deck-tabs .tab-tool .sparkle { color: var(--pf-white); }
.deck-tabs .tab-tool::after {
  content: '';
  position: absolute; top: 0; bottom: 0; width: 45%; left: -60%;
  background: linear-gradient(105deg, transparent, rgba(255, 255, 255, 0.35), transparent);
  animation: pf-shimmer 3.2s ease-in-out 0.9s 3;
}

/* ── TYPE→ENTRY entries (2026-08-02): the V1 right-zone facts (zone map,
   lands-line, rate block) speak our-products dialect — they stand down on
   third-party type entries. B3's order summary takes the zone. ── */
.deck[data-deck="mags"] #dk-map-slot,
.deck[data-deck="mags"] #dk-meta,
.deck[data-deck="mags"] #dk-pricing-slot,
.deck[data-deck="promosheets"] #dk-map-slot,
.deck[data-deck="promosheets"] #dk-meta,
.deck[data-deck="promosheets"] #dk-pricing-slot { display: none; }

/* ── FLYERS RESTRUCTURE (Jon-ruled 2026-08-03): the column opens with the
   PRODUCT STYLE, exactly as Publications opens with its publication. The
   mailshot MODE rules that used to live here are deleted with the mode
   itself — mailshots are a nav-band ticker entry now, and the broken stage
   state went with them. ── */
.pfly-style { margin-bottom: 14px; }
.pfly-stylenote { font-size: 11.5px; color: var(--pf-grey-500, #6b6b6b); margin: 6px 0 0; }
/* a coming-soon style is NAMED and visibly unavailable — never a live-looking
   option that refuses when tapped */
.pfly-stylesel option:disabled { color: var(--pf-grey-400, #9A948C); }
.pf-lbx-opt[data-disabled] { color: var(--pf-grey-400, #9A948C); cursor: not-allowed; }
.pf-lbx-opt[data-disabled]:hover { background: none; }
/* ── B6: THE B2B TOKEN — one visual element, three uses (masthead eyebrow,
   list chip, filter toggle), never a second treatment. Black, never
   orange: orange is actions only. ── */
.b2b-chip { display: inline-block; background: #1A1A1A; color: #fff; font-size: 9.5px; font-weight: 800; letter-spacing: 1.2px; padding: 2px 7px; border-radius: 4px; line-height: 1.4; font-family: inherit; }
button.b2b-chip { border: 1px solid #1A1A1A; cursor: pointer; }
/* item 7: one token family, two states — OFF = black outline, black text,
   transparent fill; ON = solid black, white text. No third treatment. */
button.b2b-chip[aria-pressed="false"] { background: transparent; color: #1A1A1A; border-color: #1A1A1A; }
/* ── B3 slice 4: multi-area (availability up front, honest refusals) ── */
.pfsh-multi { margin-top: 14px; }
.pfsh-multi h3, .pfsh-multi h4 { margin-bottom: 6px; }
.pfm-row { display: flex; align-items: center; gap: 8px; padding: 7px 0; font-size: 13px; border-bottom: 1px solid var(--pf-border); cursor: pointer; }
.pfm-row input { width: auto; margin: 0; flex: none; }
.pfm-w { margin-left: auto; color: var(--pf-grey-500, #6b6b6b); font-size: 12px; }
.pfm-none { font-size: 12.5px; color: var(--pf-grey-600, #6B6B6B); margin: 4px 0 0; }
.pfm-cant { display: block; font-size: 12px; color: var(--pf-grey-600, #6B6B6B); margin-top: 6px; }

/* ── B3 slice 3b: the visible cart + in-page payment (same-page law) ── */
.pfc-list { margin: 10px 0 8px; border-top: 1px solid var(--pf-border); }
.pfc-item { display: flex; align-items: baseline; gap: 8px; padding: 8px 0; border-bottom: 1px solid var(--pf-border); font-size: 13px; }
.pfc-l { flex: 1; }
.pfc-p { font-weight: 800; white-space: nowrap; }
.pfc-x { flex: none; border: 0; background: none; color: var(--pf-grey-500, #6b6b6b); font-size: 13px; line-height: 1; cursor: pointer; padding: 2px 4px; border-radius: var(--r-btn); }
.pfc-x:hover { color: var(--pf-black); }
.pfc-refuse { font-size: 12.5px; color: var(--pf-grey-600, #6B6B6B); margin: 8px 0 0; }
#pfc-checkout { margin-top: 10px; }
.pfc-thanks { display: block; background: var(--pf-orange-tint); border: 1px solid var(--pf-orange); border-radius: var(--r-btn); padding: 12px 14px; margin-bottom: 14px; }
.pfc-thanks b { display: block; font-size: 15px; }
/* ── W1 (Jon's walk round 1) ─────────────────────────────────────────────── */

/* W1/3 — STAGE CONSISTENCY. The stage spans every right-column row and centres
   its artefact inside that span, so when the checkout frame grew the right
   column the artefact re-centred DOWNWARD and left a void at the top. While
   the checkout is open the stage holds the exact height it had, and stops
   stretching — so the artefact does not move at all. */
.artefact-stage.stage-pinned { align-self: start; }

/* W1/2 — the phone must never hold an order with no visible way to finish it.
   The bar carries the live total and the pay control; it stands down once the
   checkout frame is up, so it can never cover the payment fields.
   TRANSPARENCY AT EVERY WIDTH (rider, Jon's capture 2026-08-05): the bar is
   a GRID — the line list owns a full row of its own, the total and the pay
   control share the row beneath — so the pay button can never sit on top of
   a crushed line. Line text ellipsizes inside its OWN box; the price and the
   per-line delete never shrink. */
.pfc-sticky { display: none; }
@media (max-width: 900px) {
  .pfc-sticky {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 300;
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    align-items: center; gap: 6px 12px; padding: 10px 14px;
    padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    background: var(--pf-white, #fff); border-top: 1px solid var(--pf-border);
    box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.10);
  }
  .pfc-sticky[hidden] { display: none; }
  body.has-pfc-sticky { padding-bottom: 118px; }
  .pfc-sb-list { grid-column: 1 / -1; min-width: 0; display: flex; flex-direction: column; gap: 2px; max-height: 96px; overflow-y: auto; }
  .pfc-sb-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
  .pfc-sb-w { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
  .pfc-sb-spec, .pfc-sb-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .pfc-sb-spec { font-size: 12.5px; font-weight: 800; }
  .pfc-sb-sub { font-size: 11px; color: var(--pf-grey-600, #6B6B6B); }
  .pfc-sb-p { flex: none; font-size: 12.5px; font-weight: 800; }
  .pfc-sb-row .pfc-x { flex: none; min-width: 36px; min-height: 36px; }
  .pfc-sb-l { grid-column: 1; display: flex; flex-direction: column; min-width: 0; }
  .pfc-sb-n { font-size: 11.5px; font-weight: 700; color: var(--pf-grey-600, #6B6B6B); }
  .pfc-sb-t { font-size: 16px; font-weight: 900; letter-spacing: -0.2px; }
  .pfc-sb-btn {
    grid-column: 2; min-height: 44px; padding: 11px 18px; border: 0;
    border-radius: var(--r-btn); background: var(--pf-orange, #FD3601); color: #fff;
    font: 800 14px/1 Inter, system-ui, sans-serif; cursor: pointer; white-space: nowrap;
  }
  .pfc-sb-btn[disabled] { background: var(--pf-grey-400, #9A948C); cursor: not-allowed; }
}

/* W1/5 — the artwork step is foretold before payment, never sprung after it */
.pfc-next { font-size: 12px; color: var(--pf-grey-600, #6B6B6B); margin: 8px 0 0; line-height: 1.35; }

/* W1/4 — the confirmation is the next STAGE of the order, mounted where the
   checkout was. It must not change the width of anything around it: the old
   build inserted into the summary column and dragged the header geometry. */
.pfc-done {
  border: 1px solid var(--pf-border); border-radius: var(--r-btn);
  background: var(--pf-white, #fff); padding: 16px; max-width: 100%;
  opacity: 0; transform: translateY(6px); transition: opacity .35s ease, transform .35s ease;
}
.pfc-done.is-in { opacity: 1; transform: none; }
.pfc-done-head { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; align-items: center; }
.pfc-plane { grid-row: 1 / 3; display: inline-flex; }
.pfc-done-head b { font-size: 16px; font-weight: 900; letter-spacing: -0.2px; }
.pfc-done-sub { font-size: 13px; color: var(--pf-grey-600, #6B6B6B); }
.pfc-receipt { margin: 14px 0 0; border-top: 1px solid var(--pf-border); }
.pfc-receipt > div { display: flex; justify-content: space-between; gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--pf-border); }
.pfc-receipt dt { font-size: 13px; color: var(--pf-grey-600, #6B6B6B); overflow-wrap: anywhere; }
.pfc-receipt dd { font-size: 13px; font-weight: 700; margin: 0; white-space: nowrap; }
.pfc-receipt .pfc-r-total dt, .pfc-receipt .pfc-r-total dd { font-weight: 900; color: var(--pf-black); }
.pfc-done-acts { margin-top: 12px; }
.pfc-print {
  min-height: 44px; padding: 10px 16px; border: 2px solid var(--pf-border);
  border-radius: var(--r-btn); background: var(--pf-white, #fff);
  font: 800 13px/1 Inter, system-ui, sans-serif; cursor: pointer;
}
.pfc-print:hover { border-color: var(--pf-orange, #FD3601); }
@media print {
  body > *:not(.deck) { display: none !important; }
  .pfc-sticky, .pfa-pick, .pfc-print { display: none !important; }
}
/* THE RECEIPT'S OWN PRINT (Jon's walk, 2026-08-24). It never shows on screen —
   it exists for the length of one print and is removed again. The rule above
   keeps only the deck, which is why the receipt printed blank once it moved
   into the money dialog; these are more specific and win. */
#pf-print-receipt { display: none; }
@media print {
  body.pf-printing > *:not(#pf-print-receipt) { display: none !important; }
  body.pf-printing #pf-print-receipt { display: block !important; }
  body.pf-printing #pf-print-receipt .pfc-print,
  body.pf-printing #pf-print-receipt .pfc-new,
  body.pf-printing #pf-print-receipt .pfc-done-acts { display: none !important; }
  body.pf-printing #pf-print-receipt .pfc-done-art img { max-height: 120mm; width: auto; }
}

/* W1/6 — two-layer refusals: one plain sentence and the fix, with the real
   numbers a tap away. Present, never prominent. */
.pfa-lead { display: block; font-weight: 800; }
.pfa-plain { display: block; margin-top: 3px; font-weight: 500; color: var(--pf-grey-600, #6B6B6B); }
.pfa-state.pfa-no .pfa-lead { color: #c22c00; }
.pfa-why { margin-top: 6px; }
.pfa-why summary { cursor: pointer; font-size: 11.5px; font-weight: 700; color: var(--pf-grey-600, #6B6B6B); }
.pfa-why span { display: block; margin-top: 4px; font-size: 11.5px; color: var(--pf-grey-600, #6B6B6B); line-height: 1.35; }

/* W1/8 — the close: their artwork, in their space, and what happens next */
.pfa-close {
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--pf-border);
  opacity: 0; transform: translateY(6px); transition: opacity .4s ease, transform .4s ease;
}
.pfa-close.is-in { opacity: 1; transform: none; }
.pfa-close-h { font-size: 13.5px; font-weight: 900; margin: 0 0 8px; letter-spacing: -0.1px; }
.pfa-close-art { border: 1px solid var(--pf-border); border-radius: 0; /* paper ruling */ overflow: hidden; background: var(--pf-grey-50, #F9F9F9); }
.pfa-close-art img { display: block; width: 100%; height: auto; }
.pfa-close-next { margin: 10px 0 0; padding-left: 16px; }
.pfa-close-next li { font-size: 12.5px; color: var(--pf-grey-600, #6B6B6B); line-height: 1.4; margin-bottom: 4px; }
.pfa-close-next b { color: var(--pf-black); }

/* W3/4 — FIELD FOCUS, unclipped. The details fields sit 8px apart, and the
   focus ring was drawn OUTSIDE the box, so it overlapped the field above and
   read as a clipped border bleeding upward. The ring is now drawn INSIDE the
   element (inset shadow + a same-width border) so it can never touch a
   neighbour, and the fields carry their own stacking so nothing crops it.
   Applies to every control in the details card, not just the offer field. */
.pfsh-details .pfsh-in,
.pfsh-details .pfsh-tradebtn,
.pfsh-details .pfsh-tradesearch { position: relative; z-index: 0; }
.pfsh-in:focus,
.pfsh-in:focus-visible {
  outline: none;
  border-color: var(--pf-orange, #FD3601);
  box-shadow: inset 0 0 0 2px var(--pf-orange, #FD3601);
  z-index: 1;
}
.pfsh-tradebtn:focus-visible {
  outline: none;
  border-color: var(--pf-orange, #FD3601);
  box-shadow: inset 0 0 0 2px var(--pf-orange, #FD3601);
  z-index: 1;
}
.pfsh-tradesearch:focus, .pfsh-tradesearch:focus-visible {
  outline: none; box-shadow: inset 0 0 0 2px var(--pf-orange, #FD3601);
}

/* W3/5 — MORE AREAS, explained. Bare postcode checkboxes meant nothing; this
   is a selling feature and has to read as one. */
.pfm-lead { font-size: 12.5px; color: var(--pf-grey-600, #6B6B6B); margin: 0 0 8px; line-height: 1.4; }
.pfm-row .pfm-add { margin-left: auto; font-size: 12px; font-weight: 800; color: var(--pf-orange, #FD3601); white-space: nowrap; }
.pfm-row input:checked ~ .pfm-add { color: var(--pf-black); }
.pfm-total { font-size: 12.5px; font-weight: 800; margin: 8px 0 0; }
.pfm-total[hidden] { display: none; }

/* (the in-column progress spine is RETIRED — superseded by the GUIDE BAR;
   the card highlight survives, gated on the hold per §4) */
.pfsh-railcard.is-next { box-shadow: inset 0 0 0 2px var(--pf-orange, #FD3601); }

/* ── THE GUIDE BAR (spec v1, form B). Black; the countdown is the ONE
   orange thing; completed states quiet; mobile = sticky top, one line,
   the countdown compresses before the words do (words ellipsize, the
   clock never clips). ── */
#pf-guide {
  position: sticky; top: 0; z-index: 400;
  display: flex; align-items: center; justify-content: center; gap: 10px;
  background: #111; color: #fff;
  font: 600 13px/1.3 Inter, system-ui, sans-serif;
  padding: 9px 14px; white-space: nowrap; overflow: hidden;
}
#pf-guide[hidden] { display: none; }
.pfg-sys { flex: none; font-weight: 700; opacity: 0.9; }
.pfg-sys:empty, .pfg-sys[hidden] { display: none; }
.pfg-words { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.pfg-words b.pfg-t { color: var(--pf-orange, #FD3601); font-weight: 800; display: inline-block; }
/* THE ✦ AI STAR (guidance law): the customer can see they are being
   guided by a bot. Its own slot — more AI rides this bar before launch. */
.pfg-ai { flex: none; display: inline-flex; align-items: center; color: var(--pf-orange, #FD3601); }
.pfg-ai[hidden] { display: none; }
/* THE THREE-STEP SPINE: the inactive steps recede, the live one leads */
.pfg-step { opacity: .55; font-weight: 600; }
/* a DONE step in the bar wears the page's green and its tick, at full
   strength — the same completion, said in the same colour */
/* THE BAR ADDS A TICK, NOT A COLOUR (Jon-ruled 2026-08-10; supersedes
   the colour mirroring of v73). The step words stay in the bar's normal
   register and completion is said by the tick alone. */
.pfg-step.pfg-did { color: inherit; opacity: 1; }
.pfg-sep { opacity: .4; margin: 0 6px; }
@media (max-width: 768px) {
  #pf-guide { font-size: 12px; padding: 8px 10px; gap: 8px; }
}

/* THE THUMB MUST NOT LOOK LIKE A DEAD IMAGE (Jon-ruled 2026-08-04). It is the
   view; the overlay is the touch surface. This is the way in — visible,
   full-size, and it says what tapping does. */
/* WHY THERE IS NO `#dk-hero-thumb { position: relative }` HERE — it is the bug
   Jon photographed (capture 2: no artefact, two pills). That ID rule outranked
   `.hero-tile .hero-thumb { position: absolute; inset: 0 }`, so the thumb fell
   back to relative, `inset` stopped sizing it, and its only remaining child
   (the absolutely-positioned clone) contributed no height — a 163x0 box that
   clipped the artefact away entirely. The button anchors to `.hero-tile`,
   which already has a real box, so the thumb needs no positioning of its own.
   Never re-add it. */
/* THE PILL AND ITS RULES ARE RETIRED (Jon-ruled 2026-08-27): "the shape cards
   are the choice at every width ... the sheet becomes what it already is on
   desktop, a sample of what they are buying into." The thumbnail is a picture
   again, so it no longer wears a pressable cursor or a scrim inviting a tap. */

/* ── YOUR SPACE, RESERVED — the cell the ENGINE held, lit up (Jon, asked
   three times; the JS shipped without this and was therefore invisible).
   Orange because a live reservation is the customer's own action showing
   itself. Unmistakable at a glance, at every width. ── */
/* (The per-cell is-mine highlight and its "Reserved" tag are gone —
   Jon-ruled 2026-09-02: a hold is a SHAPE, the sheet is a preview, and no
   cell is dressed as the customer's. The reservation lives in #pfsh-held.) */
/* ── YOUR SPACE, IN THE ORDER (Jon-ruled 2026-08-04). It appears the moment
   the space is reserved and stays through details and pay, so the customer
   never has to scroll back to the artefact to check it is still theirs. ── */
#pfsh-held:not([hidden]) {
  display: flex; flex-direction: column; gap: 2px;
  margin: 0 0 10px; padding: 8px 10px;
  border: 1px solid var(--pf-orange, #FD3601); border-radius: var(--s-1);
  background: var(--pf-orange-tint, rgba(253, 54, 1, 0.08));
}
.pfsh-held-t { font-size: 12px; font-weight: 900; letter-spacing: -.1px; color: var(--pf-orange, #FD3601); }
.pfsh-held-w { font-size: 12px; font-weight: 700; }
.pfsh-held-c { font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; opacity: .78; }

/* (The per-cell "Reserving…" pending state went with the cell-tap chooser and
   the position choice — Jon-ruled 2026-08-27 / 2026-09-02: no cell carries a
   customer state; the in-flight wait belongs to the shape card and the rail.) */

/* W2 — a HELD cell on the public sheet. Its own state: not the sold
   treatment, not an open "advertise here" invitation. Dashed because it is
   in-between, and it says exactly what is happening. */
.pfsh-cell.pfsh-holding {
  display: flex; align-items: center; justify-content: center; text-align: center;
  background: repeating-linear-gradient(45deg, #F7F4F0, #F7F4F0 6px, #F1ECE6 6px, #F1ECE6 12px);
  border: 1px dashed var(--pf-grey-400, #9A948C);
}
.pfsh-holdlabel { font-size: 8px; font-weight: 700; line-height: 1.15; color: var(--pf-grey-600, #6B6B6B); padding: 2px; }

/* W2 — THE HOLD, said honestly. The countdown comes from the server's own
   remaining seconds, so the words can never outlive what is enforced. */
.pfsh-hold {
  display: block; margin: 0 0 10px; padding: 8px 11px;
  background: var(--pf-orange-tint, rgba(253, 54, 1, 0.10));
  border-left: 3px solid var(--pf-orange, #FD3601); border-radius: 4px;
  font-size: 12.5px; font-weight: 700; color: var(--pf-black);
  font-variant-numeric: tabular-nums; /* the clock must not jitter as it ticks */
}
.pfsh-hold[hidden] { display: none; }
.pfsh-hold-gone { background: var(--pf-grey-50, #F9F9F9); border-left-color: var(--pf-grey-400, #9A948C); color: var(--pf-grey-600, #6B6B6B); font-weight: 500; }

/* W1/1 — TRADE PICKER in the booking rail. It has to read as one of the
   details fields (same height, same border, same radius token) while being a
   tap target on a phone, not a text box that summons a keyboard. */
.pfsh-trade { position: relative; }
.pfsh-tradebtn {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; min-height: 44px; padding: 10px 12px; text-align: left;
  border: 1px solid var(--pf-border); border-radius: var(--r-btn);
  background: var(--pf-white, #fff); font: 500 14px/1.2 Inter, system-ui, sans-serif;
  color: var(--pf-grey-600, #6B6B6B); cursor: pointer;
}
.pfsh-tradebtn[data-picked="1"] { color: var(--pf-black); font-weight: 700; }
.pfsh-tradebtn:hover { border-color: var(--pf-orange, #FD3601); }
.pfsh-tradebtn:focus-visible { outline: none; border-color: var(--pf-orange, #FD3601); box-shadow: 0 0 0 3px var(--pf-orange-tint, rgba(253, 54, 1, 0.18)); }
.pfsh-tradechev { color: var(--pf-grey-400, #9A948C); font-size: 12px; }
.pfsh-tradesearch {
  display: block; width: 100%; min-height: 44px; margin-top: 6px; padding: 10px 12px;
  border: 1px solid var(--pf-orange, #FD3601); border-radius: var(--r-btn);
  font: 500 14px/1.2 Inter, system-ui, sans-serif;
}
.pfsh-tradesearch[hidden] { display: none; }
.pfsh-catlist { max-height: 46vh; }
.pfsh-catlist .ci { min-height: 44px; align-items: center; }

/* artwork intake, in the page — the upload lives in the confirmation
   because artwork arrives AFTER payment (same-page law) */
.pfa-wrap { margin-top: 12px; }
.pfa-h { font-size: 12px; font-weight: 800; letter-spacing: 0.4px; text-transform: uppercase; color: var(--pf-grey-600, #6B6B6B); margin: 0 0 8px; }
.pfa-item { border-top: 1px solid var(--pf-border); padding: 10px 0 4px; }
.pfa-what { font-size: 13.5px; font-weight: 800; margin: 0; }
.pfa-need { font-size: 12px; color: var(--pf-grey-600, #6B6B6B); margin: 3px 0 8px; overflow-wrap: anywhere; }
/* min-height 44 = the tap-target floor, at EVERY breakpoint — this control is
   pressed on a phone more than anywhere else (artwork comes off the camera roll) */
.pfa-pick { position: relative; display: inline-flex; align-items: center; gap: 8px; min-height: 44px; border: 2px solid var(--pf-border); border-radius: var(--r-btn); padding: 8px 14px; font-size: 13px; font-weight: 800; cursor: pointer; background: var(--pf-white, #fff); }
.pfa-pick:hover { border-color: var(--pf-orange, #FD3601); }
.pfa-pick:focus-within { border-color: var(--pf-orange, #FD3601); box-shadow: 0 0 0 3px var(--pf-orange-tint, rgba(253, 54, 1, 0.18)); }
.pfa-pick input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.pfa-state { font-size: 12.5px; margin: 8px 0 0; overflow-wrap: anywhere; }
.pfa-state.pfa-yes { color: var(--pf-green); font-weight: 700; }
.pfa-state.pfa-no { color: #c22c00; }
.pfc-thanks span { display: block; font-size: 13px; color: var(--pf-grey-600, #6B6B6B); margin-top: 2px; }

/* ── MOBILE BUNDLE 2 (2026-08-03) ── */
/* item 4 (ratified two-tone): the strip's PromoFlyers label takes the
   LOGO'S two-tone wordmark treatment — orange inside the WORDMARK is
   BRAND, not action (named exception, CLAUDE.md). The active underline
   stays the state signal. */
.lbs-item .lbs-two b { color: var(--pf-orange, #FD3601); font-weight: inherit; }
/* item 2: one-line mobile band — the short label serves ≤600 */
.lbs-short { display: none; }
@media (max-width: 600px) {
  .lbs-full { display: none; }
  .lbs-short { display: inline; }
  .loc-band .lb-strip { gap: 4px; }
  .loc-band .lbs-item { padding-left: 7px; padding-right: 7px; letter-spacing: -0.01em; }
  /* item 3: Design + B2B share one row and one baseline on phone — same
     flex order as Design (DOM order seats B2B directly after it). The
     location control takes its already-ratified 601–940px compression
     ("📍 Bristol ⌄") here too: 179px of ruled copy + Design + B2B cannot
     share 327px; the compressed control is the ratified answer (ledgered). */
  #b2b-toggle.tab-b2b { order: 2; margin-left: 8px; }
  .lb-ln { display: none; }
  /* item 1: overlay tap targets meet the 44px minimum */
  #pfsh-lbx .pfsh-cell { min-height: 44px; }
}
/* item 3: focus uses the token's treatment, never a browser default */
#b2b-toggle:focus { outline: none; }
#b2b-toggle:focus-visible { outline: 2px solid var(--pf-orange, #FD3601); outline-offset: 2px; }
/* item 5: ONE corner token at every breakpoint — native selects included
   (mobile keeps the OS picker but not the OS corner) */
.pfsh-pubsel, .pfmg-sel, #dk-area, #dk-week, #dk-offer-type { border-radius: var(--r-btn); }

/* placement REVISED (2026-08-02): the mode switch sits AFTER Design — same
   shape language, SMALLER, clearly subordinate. Resting = solid black,
   white text (Jon). ACTIVE = PROPOSED: the site's orange active-underline
   state signal (strip precedent) — both states captured for Jon's ruling. */
/* item 1 (Flyers capture): B2B matches Design — the ONE house corner token
   (10px, same as .tab-tool/listboxes/term cards), shared padding rhythm +
   font treatment (weight 800), same baseline; smaller and subordinate. */
/* Jon's capture item (2026-08-03): B2B height = Design height EXACTLY —
   width and weight distinguish it, never height. Same borders, same
   vertical padding, and the line-height fills the same text box Design's
   larger type occupies; inline-flex centres the smaller glyphs. */
#b2b-toggle.tab-b2b { flex: none; align-self: center; margin-left: 8px; display: inline-flex; align-items: center; border-radius: 10px; border-width: 2px; padding: 8px 12px; font-size: 11px; font-weight: 800; letter-spacing: 0.8px; line-height: 17px; }
@media (max-width: 600px) {
  #b2b-toggle.tab-b2b { padding: 6px 9px; line-height: 16px; }
}
#b2b-toggle.tab-b2b[aria-pressed="false"] { background: #1A1A1A; color: #fff; border-color: #1A1A1A; }
#b2b-toggle.tab-b2b[aria-pressed="true"] { background: #1A1A1A; color: #fff; border-color: #1A1A1A; box-shadow: 0 3px 0 0 var(--pf-orange, #FD3601); }
.pfmg-resetnote { font-size: 12px; color: var(--pf-grey-500, #6b6b6b); margin: 0 0 8px; }

/* ── custom listboxes: on-brand, ROUNDED, one border language with the
   location selector and the Single/Double/Quad family ── */
.pf-lbx-wrap { position: relative; }
.pf-lbx-btn { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; background: var(--pf-white, #fff); border: 1px solid var(--pf-border, #E8E8E8); border-radius: 10px; padding: 10px 12px; font: inherit; font-size: 14px; font-weight: 600; cursor: pointer; }
.pf-lbx-btn:focus-visible { outline: 2px solid var(--pf-orange, #FD3601); outline-offset: 2px; }
.pf-lbx-caret { margin-left: auto; color: var(--pf-grey-500, #6b6b6b); flex: none; }
.pf-lbx-pop { position: absolute; z-index: 60; left: 0; right: 0; margin-top: 6px; background: var(--pf-white, #fff); border: 1px solid var(--pf-border, #E8E8E8); border-radius: 10px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); max-height: 280px; overflow: auto; padding: 4px; }
.pf-lbx-opt { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; background: none; border: 0; border-radius: 8px; padding: 9px 10px; font: inherit; font-size: 14px; cursor: pointer; }
.pf-lbx-opt:hover { background: var(--pf-cream, #FAF6F2); }
.pf-lbx-opt[aria-selected="true"] { font-weight: 800; }
.pf-lbx-opt .b2b-chip, .pf-lbx-btn .b2b-chip { margin-left: auto; flex: none; }
.pf-lbx-btn .b2b-chip { margin-left: 8px; }
.pf-lbx-group { font-size: 11px; font-weight: 800; letter-spacing: 0.5px; text-transform: uppercase; color: var(--pf-grey-500, #6b6b6b); padding: 8px 10px 2px; }
/* mobile call (ledgered): the NATIVE select serves ≤600px — OS pickers */
@media (min-width: 601px) {
  .pf-lbx-native { position: absolute !important; width: 1px; height: 1px; opacity: 0; pointer-events: none; overflow: hidden; }
}
@media (max-width: 600px) {
  .pf-lbx-btn, .pf-lbx-pop { display: none !important; }
}
/* masthead eyebrow: composes ONLY when AUDIENCE=B2B — B2C bands take no
   change of any kind */
.pfsh-band .b2b-eyebrow { flex: 1 0 100%; width: max-content; margin-bottom: 1px; }
.pfsh-band.b2b-band { flex-wrap: wrap; }
.pfmg-empty { text-align: center; color: var(--pf-grey-500, #6b6b6b); font-size: 13px; padding: 18px 12px; }

/* (the mailshot-mode right-column stand-down rules were deleted with the mode
   — the solo facts are now the only thing this tab ever speaks) */

/* ── NO GHOST OF THE SUPERSEDED CARD (Jon-ruled 2026-08-04, finding 4). The V1
   composed card is still the real artefact on V1 areas, so it stays in the
   markup — but on a sheet area it was painting in the FIRST FRAME and being
   removed afterwards, which is precisely what the superseded-elements law
   forbids. It is hidden until the deck resolves which artefact this area
   gets, then revealed. `visibility` not `display`: the stage keeps its box,
   so nothing reflows when the answer arrives.
   Degradation: with JS off the class never lands, so the <noscript> rule in
   the head shows the card exactly as before. ── */
/* The V1 composed card is GONE from the served markup (slice 3). The rules
   that used to hide it are deleted with it — hiding is what leaked. */

/* ── COMING SOON + REGISTER INTEREST (readiness gate, Jon-ruled 2026-08-04).
   It occupies the stage where the artefact would be, because there IS no
   artefact for an area we cannot sell — that is the honest statement, not a
   gap. No price, no counter, no availability, no date. ── */
html.pf-soon-live .artefact-stage .cc-face,
html.pf-soon-live:not(.pf-near-live) #dk-hero-thumb .pfsh-tileclone { display: none !important; }
/* COMPACT LABELLED SECONDARY (deck-model reconciliation, Jon-ruled
   2026-08-06): one line plus email beside the guest publication — never a
   dominant panel, never a headline block */
#pf-soon {
  display: flex; flex-direction: column; gap: 8px;
  margin: 10px 0; padding: 10px 12px;
  border: 1px solid var(--pf-border); border-radius: 8px;
  background: var(--pf-grey-50);
}
.pf-soon-line { font-size: 13px; font-weight: 700; line-height: 1.4; }
/* the credits row: lawful minimum, one tidy line beside the year line —
   small enough that 390 wraps at most once, never a stacked block */
.footer-attrib { font-size: 10.5px; line-height: 1.5; color: var(--pf-grey-500, #8a857d); margin-top: 4px; flex-basis: 100%; }
.footer-attrib a { color: inherit; text-decoration: underline; padding-block: 0; } /* the nav-link tap padding would balloon the credits line */

/* REFUSAL-WITH-RECOVERY (Jon's walk ruling): the refuted postcode speaks AT
   the input — attention register (ratified for small text, AA), one line,
   the correction path always rendered */
.pc-refute { margin: 6px 0 0; font-size: 12.5px; font-weight: 700; color: var(--pf-attention); }
/* PENDING LOOKS BUSY (the contract's third state): while the verdict is in
   flight the field says so — a quiet pulse, never a silent ignore */
@keyframes pf-pc-busy { 0%, 100% { border-color: var(--pf-border); } 50% { border-color: var(--pf-grey-400, #9A948C); } }
html:not(.pf-avail-resolved) #dk-postcode { animation: pf-pc-busy 1s ease-in-out infinite; }
#dk-postcode[aria-invalid="true"] { border-color: var(--pf-attention); }
#dk-postcode[aria-invalid="true"]:focus-visible { outline-color: var(--pf-attention); }

/* UK-WIDE PICKER (Jon-ruled): type-ahead at the top of the location menu;
   the 120-area list is unusable bare — the search makes it one gesture */
.lb-search { position: sticky; top: 0; background: var(--pf-white); padding: 8px 10px 6px; z-index: 2; }
.lb-search input {
  width: 100%; box-sizing: border-box; padding: 9px 12px; font: inherit; font-size: 14px;
  border: 1px solid var(--pf-border); border-radius: 8px;
}
.lb-code { font-size: 11px; font-weight: 700; color: var(--pf-grey-500, #8a857d); margin-left: 6px; }
.lb-none { padding: 10px 12px; margin: 0; }
/* fingers get the ratified 44px floor on the picker rows */
@media (max-width: 768px) {
  #lb-loc .lb-item { min-height: 44px; }
}
#pf-soon p { margin: 0; font-size: 14px; line-height: 1.45; }
.pf-soon-f { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.pf-soon-l { font-size: 12px; font-weight: 800; }
/* ONE ASK — the compact inline interest capture (interest dedupe, Jon-ruled
   2026-08-06): reveals in place of its link; single-line input + button at
   NORMAL control sizing (the .pfsh-in idiom), consent line beneath,
   390-clean by flex-wrap inside the note's own width */
.pf-int { display: inline; }
.pf-int-go { font-weight: 800; white-space: nowrap; }
.pf-int-f { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.pf-int-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.pf-int-row input[type="email"] {
  flex: 1 1 170px; min-width: 0; font: inherit; font-size: 14px;
  padding: 10px 11px; border: 1px solid var(--pf-border);
  border-radius: 10px; background: var(--pf-white, #fff);
}
.pf-int-row .pfsh-cta { flex: none; width: auto; padding: 9px 14px; font-size: 13px; }
.pf-int-c { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 400; }
.pf-int-say { font-size: 13px; font-weight: 700; margin: 0; min-height: 1.2em; }
.pf-soon-c { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.pf-soon-say { font-size: 13px; font-weight: 700; min-height: 1.2em; }
@media (max-width: 900px) { #pf-soon { padding: 16px 14px; } #pf-soon h3 { font-size: 17px; } }

/* A COMING-SOON AREA SELLS NOTHING, AND SAYS NOTHING ABOUT SUPPLY. The V1
   card's own scarcity line ("0 of 8 taken · Central & West Bristol") was
   still rendering on the phone for an area with no edition at all — a
   superseded product quoting availability it does not have. The whole
   ordering surface goes with it: no artefact, no counter, no price, no rail.
   (Launch-lattice copy law: never a counter, never availability, never a
   price on a coming-soon area.) */
/* Hiding classes one at a time was whack-a-mole and I missed three: a
   `scarcity-line` counter, a "Reserve my space - PS199" button, and a hero
   line quoting "from PS49" — all on an area with no edition. The rule is
   inverted instead: on a coming-soon area the ordering column contains the
   COMING-SOON PANEL AND NOTHING ELSE, so a surface I have never seen cannot
   leak either. */
html.pf-soon-live:not(.pf-near-live) .deck-pane[data-pane="card"] > *:not(#pf-soon) { display: none !important; }
/* under a GUEST the card pane carries the guest's real booking rail —
   .pfsh-left-live already stands the V1 steps down inside it */
/* ...and the RIGHT rail, which I missed on the first inversion: the price
   chip lives in `.pricing-slot` there, so an area with no edition was still
   quoting PS199. The right column is context and confirmation — on a
   coming-soon area there is no order to confirm and no rate to quote. */
/* PRODUCT-AWARE CONTAINMENT (availability slice §1, Jon-ruled 2026-08-05):
   coming-soon is the SHEET'S verdict, not the deck's. The solo flyer is
   available UK-WIDE under its own channel model, so the Flyers column — its
   steps, its pricing dock, its sticky CTA and the cart bar — LIVES here:
   the BS7 half-deck is dead. What still hides is sheet-selling surface
   only, and even that yields when a nearest live publication renders as a
   labelled GUEST (html.pf-near-live, §4). The old unqualified .cfg-step /
   .artefact-stage / .sticky-cta rules were the half-deck's mechanism.
   (.dk-steps is retired — it matched no served markup; orphan-ledgered.) */
html.pf-soon-live:not(.pf-near-live) #pfsh-sum,
html.pf-soon-live:not(.pf-near-live) #pfsh-rail { display: none !important; }
/* the sheet's stage on the PromoFlyers tab: absent without a guest — never
   a blank frame dressed as a product */
html.pf-soon-live:not(.pf-near-live) .deck[data-deck="card"] .artefact-stage { display: none !important; }
html.pf-soon-live #pf-soon { margin-block: 6px 14px; }

/* The card tab's sticky Reserve CTA hides while the sheet is not sellable
   and no guest stands; the SOLO sticky CTA and the CART bar stay — the solo
   product is sellable here and a held order must always show its way out
   (W1/2). */
html.pf-soon-live:not(.pf-near-live) .deck[data-deck="card"] .sticky-cta { display: none !important; }

/* ── THE HONEST FAILURE STATE (Jon-approved 2026-08-04). A network failure is
   not a product. It gets the mark, one plain line and a retry — no price, no
   availability, no superseded card, and never a blank stage. Same containment
   as coming-soon: the ordering surfaces go, because none of them can be
   trusted while we cannot read the sheet. ── */
html.pf-failed-live .deck-pane[data-pane="card"] > *:not(#pf-failed) { display: none !important; }
/* ── DATA MUST NEVER BE ABLE TO REMOVE SOLO'S ABILITY TO PAY (Jon-ruled
   2026-08-26) ───────────────────────────────────────────────────────────
   `sheetFailed()` is the SHEET's failure state and says so in its own
   comment — "it speaks in the card pane only, never over the Flyers column".
   THE JS ALREADY KNEW. THE CSS NEVER DID: these selectors were global, so a
   Shared-side data condition took out SOLO's price lines, its total and its
   Book my drop button, at every width.

   MEASURED, 3 runs of 3: BS3's only Shared drop sits in `Planning`, so
   `/api/slots/areas` answers `{"areas":[]}`, the area select never receives a
   value, `/api/sheet/for-area` is NEVER FETCHED, the 5s failsafe fires at
   ~5.3s and `Book my drop` measures 0x0 — on a product that needs no Shared
   drop at all (`for-area` reports `solo.available: true` in the same breath).

   The pay-critical furniture is now scoped to the pane the failure belongs
   to. Solo prices, totals and pays through a Shared outage.
   Gated: `gate-solo-pays-through-failure.js`. */
html.pf-failed-live .hero-row,
html.pf-failed-live .artefact-stage,
html.pf-failed-live .scarcity-line,
html.pf-failed-live .deck:not([data-deck="solo"]) .pricing-slot,
html.pf-failed-live .deck:not([data-deck="solo"]) .rail-price,
html.pf-failed-live #pfsh-sum,
html.pf-failed-live .deck:not([data-deck="solo"]) .sticky-cta,
html.pf-failed-live .pfc-sticky,
/* the two I forgot to carry over from coming-soon, and the gate caught:
   the advisor's "from £49" line and the V1 counters */
html.pf-failed-live .pfc-t,
html.pf-failed-live .pfc-scarce,
html.pf-failed-live .deck:not([data-deck="solo"]) .cfg-step { display: none !important; }
/* NEUTRAL-SHIP (availability slice §3, ratified): until the availability
   verdict lands, the ordering zones carry NO price and NO order control —
   the deck reveals ONCE, to whichever state is true. visibility (not
   display) so geometry is reserved and the reveal never jumps the page.
   The class is re-armed on every area change: a stale area's prices can
   never dress the new area. */
html:not(.pf-avail-resolved) .cfg-step:not(:focus-within),
html:not(.pf-avail-resolved) .pricing-slot,
html:not(.pf-avail-resolved) .rail-price,
html:not(.pf-avail-resolved) #pfsh-sum,
html:not(.pf-avail-resolved) #pfsh-rail,
html:not(.pf-avail-resolved) .sticky-cta,
html:not(.pf-avail-resolved) #dk-map-note { visibility: hidden !important; }
/* the CART BAR is deliberately NOT neutral-shipped: it prices an order the
   customer already holds — the area verdict cannot change it, and W1/2
   forbids ever hiding a held order's way out */

/* THE GUEST LABEL (availability slice §4) — load-bearing honesty: the
   customer must always know WHICH area's publication they are buying. */
#pf-guest-note {
  margin: 0 0 10px; padding: 10px 14px; border: 1px solid var(--pf-border);
  border-left: 4px solid var(--pf-orange, #FD3601); border-radius: 8px;
  background: var(--pf-grey-50, #FAF8F5); font-size: 13.5px; line-height: 1.45;
}
#pf-guest-note b { font-weight: 800; }

#pf-failed {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  max-width: 420px; margin: 10px auto 16px; padding: 26px 20px; text-align: center;
  border: 1px solid var(--pf-border); border-radius: var(--s-1); background: var(--pf-grey-50);
}
.pf-failed-mark { line-height: 0; animation: pf-fly 2.4s ease-in-out infinite; }
.pf-failed-mark img { width: 44px; height: 44px; }
.pf-failed-l { margin: 0; font-size: 15px; font-weight: 700; line-height: 1.45; }
#pf-failed .pfsh-cta { width: auto; min-width: 150px; }
/* ── THE CAPTURE STAYS UP EVEN WHEN THE PANEL CANNOT (Jon-ruled 2026-08-28).
   The failure line is the loud one and keeps its weight; the offer beneath it
   is ordinary text, because it is an offer and not an alarm. The capture rides
   the same `.pf-int` treatment it has everywhere else — one mechanism, one
   look, so a customer meets the same control here as on a coming-soon area. */
.pf-failed-offer { margin: 0; font-size: 14px; line-height: 1.5; color: var(--pf-ink-2, inherit); }
#pf-failed .pf-int { font-size: 14px; }
#pf-failed .pf-int-f { display: flex; flex-direction: column; align-items: center; gap: 8px; }
/* the paper plane keeps flying while we retry — motion says "working", the
   words say what is true */
@keyframes pf-fly {
  0%   { transform: translate(-6px, 3px) rotate(-4deg); }
  50%  { transform: translate(6px, -3px) rotate(4deg); }
  100% { transform: translate(-6px, 3px) rotate(-4deg); }
}
@media (prefers-reduced-motion: reduce) { .pf-failed-mark { animation: none; } }

/* The checkout terms line sits with the pay control — small, but never hidden
   and never a hover-only footnote: the business-purposes confirmation is what
   the B2B-only sale rests on. */
.pfc-terms {
  margin: 8px 0 0; font-size: 11px; line-height: 1.45; opacity: .82;
}
.pfc-terms a { text-decoration: underline; }

/* ── ORDER TRANSPARENCY (Jon-ruled 2026-08-05). The sticky bar carries the
   LINES, not just their total — a total beside an invisible list is how
   "5 spaces · £249.75/month" came to sit under a QUAD selector and read as
   the price of the quad. Capped and scrollable so a long order cannot swallow
   the screen, but never collapsed to nothing: what you are paying for is
   always on the glass with what you are paying. ── */
.pfc-sb-list {
  max-height: 34vh; overflow-y: auto; -webkit-overflow-scrolling: touch;
  border-bottom: 1px solid rgba(255, 255, 255, .18); margin-bottom: 8px;
}
.pfc-sb-row {
  display: grid; grid-template-columns: 1fr auto auto; align-items: center;
  gap: 8px; padding: 7px 0;
}
.pfc-sb-row + .pfc-sb-row { border-top: 1px solid rgba(255, 255, 255, .10); }
.pfc-sb-w { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.pfc-sb-spec { font-size: 13px; font-weight: 800; }
.pfc-sb-sub { font-size: 11px; opacity: .78; }
.pfc-sb-p { font-size: 13px; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }
.pfc-sb-row .pfc-x {
  min-width: 32px; min-height: 32px; border: 0; background: transparent;
  color: inherit; opacity: .7; cursor: pointer; font-size: 14px; line-height: 1;
}
.pfc-sb-row .pfc-x:hover, .pfc-sb-row .pfc-x:focus-visible { opacity: 1; }

/* The order area states its own sum. Above the phone breakpoint the sticky bar
   is display:none, so without this the total existed only inside a hidden
   element — priced lines and no total is not transparency either. */
.pfc-total {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--pf-border);
  font-size: 14px; font-weight: 800;
}
.pfc-total-v { font-variant-numeric: tabular-nums; }

/* The remove control was 19×17 on desktop — it worked, and it was smaller than
   any pointer target should be (WCAG 2.2 puts the floor at 24×24). A control
   that deletes a line from an order the customer is about to pay for is not
   the place to be stingy with hit area. */
#pfsh-cart .pfc-x {
  min-width: 28px; min-height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
}

/* ── NEXT-STEP GUIDANCE NEVER RENDERS MUTED (Jon-ruled 2026-08-05) ──
   "Add your business name and trade first" is the line that tells a customer
   what to do next; it was 11px in --pf-grey-400 (#9A9A9A), which is quieter
   than the copy it interrupts. Guidance now carries the brand attention
   colour and its own weight.

   CONTRAST-CHECKED, not eyeballed. Brand orange #FD3601 measures 3.68:1 on
   white — it FAILS AA for text this size, so using it literally would trade
   one legibility problem for another. --pf-attention is the same brand red
   darkened only as far as AA requires: 5.39:1 on white, 4.99:1 on grey-50,
   the two backgrounds these lines actually sit on. The orange itself is
   untouched for fills and non-text brand use. */
:root { --pf-attention: #CC2A01; }
.pfsh-note,
.pfc-next {
  color: var(--pf-attention);
  font-weight: 700;
}
.pfsh-note { font-size: 12px; }
.pfsh-note.is-ok { color: var(--pf-green); }   /* a success is not an alert */
.pfsh-sp .pfsh-per { display: block; font-size: 10px; font-weight: 600; color: var(--pf-grey-400, #9A9A9A); line-height: 1.2; }

/* ── THE FLYER ARTEFACT — CANONICAL RENDER, SCALED (attempt 4) ──
   Every dimension here is a FIXED PIXEL VALUE. No aspect-ratio, no flex, no
   percentage height: those are exactly what three attempts watched collapse
   to 2px, 56px and 0 as different hosts declined to resolve a height. The JS
   sets width/height/transform; CSS only draws.
   The canonical frame is 856x594 — two A4 faces at 2px/mm, side by side with
   a gutter — so the artefact's ratio is a fact of the render rather than
   something a container negotiates. */
[data-pff-canvas], .pff-scaled .pff {
  display: flex; gap: 16px; padding: 12px; box-sizing: border-box;
  background: var(--pf-grey-50); border: 1px solid var(--pf-border);
  border-radius: 10px; overflow: hidden;
}
/* flex:none — a squeezed flex container must never crush the reserved box
   (the phone sheet's 10px pane compressed a 208px instance to 0) */
.pff-scaled { position: relative; overflow: hidden; flex: none; }
.pff-face {
  position: relative; flex: none; width: 402px; height: 570px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; padding: 18px; text-align: center; box-sizing: border-box;
  background: var(--pf-white); border: 1px solid var(--pf-border); border-radius: 4px;
}
.pff-art { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
.pff-art[hidden] { display: none; }
.pff-empty { font-size: 19px; line-height: 1.45; font-weight: 700; color: var(--pf-grey-500, #6b6b6b); max-width: 20ch; }
.pff-side {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 8px 0;
  font-size: 15px; font-weight: 800; letter-spacing: .6px; text-transform: uppercase;
  color: var(--pf-grey-500, #6b6b6b); background: var(--pf-grey-50);
  border-top: 1px solid var(--pf-border);
}
/* the canonical source is never displayed directly — only its scaled clones */
[data-pff-canvas] { position: absolute; left: -99999px; top: 0; width: 856px; height: 594px; }

/* ── THE DESIGN GALLERY (Jon-ruled 2026-08-09) ─────────────────────────
   Front preview, front templates beneath it; back preview, back templates
   beneath that. On a wide screen the two blocks sit side by side; on the
   phone they STACK, front block first — same behaviour, same gates, both
   classes. Each row scrolls HORIZONTALLY at thumbnails you can actually
   see; nothing shrinks to fit. */
.dz-gallery { display: grid; gap: 12px; margin-bottom: 12px; }
/* TWO COLUMNS ONLY WHEN TWO FLOORS FIT. `1fr 1fr` at 980 assumed the column
   could always hold two faces; with the floor derived from the region maps it
   sometimes cannot, and the back face escaped its section by 60px at 1240 and
   20px at 1320 — the containment gate caught what the face-size gate could
   not, because each was measuring its own half of the same rule. auto-fit
   against the derived floor drops to one column by arithmetic instead of by a
   breakpoint someone has to keep in step with the maps. */
@media (min-width: 980px) {
  .dz-gallery {
    grid-template-columns: 1fr;   /* one face at a time, Jon-ruled 2026-08-24 */
    align-items: start;
  }
}
/* THE FACE BLOCK'S GAPS ARE HALVED (Jon-ruled 2026-08-24). The caption that
   briefly stood here is DELETED — the picture names itself, and the same
   ruling that moved it over the flyer replaced it with nothing hours later.
   What the block's furniture actually is, measured, is in DECISIONS-PENDING
   2026-08-24 (e): the 146px/112px under the picture is mostly the LOOK
   CAROUSEL, not a label and a gap, and I named it wrongly in (c). */
.dz-face-block { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
/* NO HEIGHT ON THE PREVIEW HOST. The painter treats a host's height as a
   BUDGET and scales the face to fit it — so a well-meant min-height of
   120px shrank a 426px-wide face to 86px. The host states its width; the
   artefact's own proportions state the rest. */
.dz-face-prev { width: 100%; }
/* THE PREVIEW EARNS ITS SPACE (Jon's above-the-fold requirement): on the
   phone the front block has to sit inside one screen alongside the
   controls, so the artefact is capped there — it stays the biggest thing
   in the section, it simply stops being taller than the screen. */
@media (max-width: 767px) { .dz-face-prev .pff-scaled { max-height: 314px; overflow: hidden; } }
.dz-face-prev .pff-scaled { margin: 0 auto; }
.dz-tpl { margin: 0; }
.dz-tpl > label {
  display: block; margin-bottom: 4px;
  font-size: 12px; font-weight: 700; color: var(--pf-black);
}
.dz-tpl-row {
  display: flex; flex-wrap: nowrap; gap: 6px; overflow-x: auto; overflow-y: hidden;
  padding-bottom: 2px; scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch; scrollbar-width: thin;
}
.dz-tpl-row > button {
  flex: 0 0 auto; scroll-snap-align: start;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 3px; background: var(--pf-white); cursor: pointer;
  border: 2px solid var(--pf-border); border-radius: var(--r-btn);
}
/* ── THE MINI CAROUSEL (Jon-ruled 2026-08-09) ──────────────────────────
   The chooser is a LOW strip with an arrow each side, taking as little
   vertical space as it can. The thumbnail no longer has to carry the
   design — tapping one previews it LARGE in the preview above — so the
   v52 "thumbs >= 140px" sizing is SUPERSEDED here: the strip is an index,
   the preview is the picture.
   THE ARROWS ARE FINGER-SIZED: 44x44 at the phone, which is the smallest
   target a thumb reliably hits, and they never eat more height than the
   strip they sit beside. */
.dz-car { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 4px; }
.dz-car-arw {
  width: 44px; height: 44px; flex: none; display: inline-flex; align-items: center; justify-content: center;
  background: var(--pf-white); color: var(--pf-ink, #111); cursor: pointer;
  border: 1px solid var(--pf-border); border-radius: 999px;
}
.dz-car-arw:hover { border-color: var(--pf-ink, #111); }
.dz-car-arw[disabled] { opacity: .35; cursor: default; }
.dz-tpl-row > button > img { display: block; width: 64px; height: auto; border-radius: 3px; }
/* THE THUMBNAILS DO NOT GROW AT THE BREAKPOINT (Jon-ruled 2026-08-24). They
   went 64px -> 72px wide at exactly the width where the column narrows, so the
   picker got taller (101px -> 115px) in the same pixel that the row wrapped,
   and the flyer paid for both. The rule is deleted rather than overridden:
   14px of the 156px step, and one fewer thing that changes at 1200. */
.dz-tpl-row > button > span {
  font-size: 10px; font-weight: 700; line-height: 1.2; color: var(--pf-grey-600);
  max-width: 72px; text-align: center;
}
.dz-tpl-row > button[aria-pressed="true"] { border-color: var(--pf-orange); }
.dz-tpl-row > button[aria-pressed="true"] > span { color: var(--pf-black); }
.dz-tpl-row > button:hover { border-color: var(--pf-ink, #111); }

/* (the v1 "Available near you" link card — .pf-near — is RETIRED with the
   nearest-available v2 guest render, availability slice §4; the upcoming-
   areas page reuses the interest-form vocabulary instead) */
.pf-up-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 14px; margin-bottom: 8px;
  background: var(--pf-white); border: 1px solid var(--pf-border); border-radius: var(--s-1);
}
.pf-up-n { font-size: 15px; font-weight: 800; margin-right: auto; }
.pf-up-s { font-size: 12px; font-weight: 700; color: var(--pf-grey-600, #6B6B6B); }
.pf-up-row .pf-soon-f { flex-basis: 100%; }


/* ── DESIGN, COMPACTED (Jon-ruled 2026-08-09) ──────────────────────────
   "The options are eating too much height." They were: on the phone the
   brief alone ran 867px, every field spending a 28px label line, a 48px
   control and 16px of air. Density is raised INSIDE THE SECTION ONLY and
   inside the existing scale — the three-section clarity, the labels and
   every control stay exactly where they were, just tighter.
   Nothing here touches SPEC or CONFIRM, and nothing shrinks a preview:
   the picture keeps its size, the plumbing gets out of its way. */
.zone[data-zone="design"] .field { margin-bottom: 5px; }
.zone[data-zone="design"] .field > label,
.zone[data-zone="design"] .dz-step > .field > label {
  margin-bottom: 3px; font-size: 12.5px; line-height: 1.25;
}
.zone[data-zone="design"] :is(input[type="text"], input[type="tel"], input[type="url"], select) {
  height: 40px; padding-top: 0; padding-bottom: 0;
}
.zone[data-zone="design"] textarea { padding-top: 8px; padding-bottom: 8px; }
.zone[data-zone="design"] .dz-steps { gap: 8px; }
.zone[data-zone="design"] .dz-step { gap: 0; }
.zone[data-zone="design"] .hint { margin-top: 2px; line-height: 1.35; }
.dz-under { gap: 10px; margin-top: 10px; }
/* the artwork buttons and the orientation sit as ONE group, one row each,
   instead of three stacked blocks with their own air */
/* THE ARTWORK ROW IS A ROW (Jon-ruled 2026-08-23): "View full size must sit on
   ONE LINE with Upload my own flyer, Reset and the tick — responsive, both
   viewports. It is a key control because the flyer is too small to edit at
   rest." It was a one-column GRID, so every control had a line to itself on the
   phone and on the Shared product, and only desktop Solo ever looked like a row.
   Flex, wrapping, with the upload taking the slack and the three compact
   controls holding their size. Below 430px the door's LABEL is hidden to the
   eye and kept in the accessible name — the house rule for an icon-only
   control — because four full labels cannot share 308px honestly. */
.dz-under-block[data-under="solo"], .dz-under-block[data-under="card"] {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
}
.dz-under-block .seg { min-height: 40px; flex: 1 1 148px; width: auto; min-width: 0; }
.dz-under-block .dz-full, .dz-under-block .dz-reset, .dz-under-block .dz-tick { flex: 0 0 auto; }
.dz-under-block .upload-note { flex: 1 0 100%; }
/* ONE CONTROL, ONE WORD (Jon-ruled 2026-08-23): the door reads "Full size"
   everywhere, both viewports, and its label is never hidden. The word is short
   enough to share the line, which is why hiding it was only ever a workaround
   for the longer one. */
@media (max-width: 430px) {
  .dz-under-block .dz-full { padding: 8px 10px; }
  .dz-under-block .seg { flex: 1 1 96px; }
}
.dz-under-block .seg--mini { margin-top: 0 !important; }
.dz-under-block .upload-note:empty { display: none; margin: 0; }
/* the 640px grid rule is retired with the grid — the row wraps by itself now */


/* ── THE DESIGN AREA IS PRODUCT-SCOPED (Jon-ruled 2026-08-09) ──────────
   Solo shows its two faces; Shared shows its cell in the publication.
   The blocks are HIDDEN BY THE PRODUCT, never merely emptied, so nothing
   solo-specific can paint on the shared path or the other way round. */
.dz-face-block[hidden] { display: none; }
.dz-gallery:has(.dz-face-block[data-face="cell"]:not([hidden])) { grid-template-columns: 1fr; }
#dz-prev-cell .pfsh-cellclone { display: block; width: 100%; overflow: hidden; }
#dz-prev-cell .pfsh-cellclone .pfsh-sheet { transform-origin: top left; }


/* ── THE SMALLEST BRIEF, AND ONE DISCLOSURE (Jon-ruled 2026-08-10) ─────
   The design step has to be usable without scrolling, so what shows by
   default is only what the engine cannot infer — who you are, what you
   do, the offer — and the press. Everything else lives behind ONE
   disclosure, because most of it is now typed ON THE ARTEFACT anyway.
   THE SCROLLER USES ITS ARROWS: no base scrollbar, no drag strip. Swipe
   still works on touch — the furniture is what goes, not the gesture. */
.dz-essentials { display: grid; gap: 8px; margin-bottom: 8px; }
.dz-essentials .field { margin-bottom: 0; }
.dz-essentials textarea { min-height: 52px; }
.dz-ess-row { display: grid; gap: 8px; grid-template-columns: 1fr; }
@media (min-width: 360px) { .dz-ess-row { grid-template-columns: 1fr 1fr; } }
.dz-more { margin: 10px 0 0; border-top: 1px solid var(--pf-border); }
.dz-more-btn {
  width: 100%; cursor: pointer; background: none; border: 0; padding: 9px 2px;
  font: 700 12.5px/1.3 Inter, system-ui, sans-serif; color: var(--pf-grey-600);
  display: flex; align-items: center; gap: 6px; text-align: left;
}
.dz-more-btn::after {
  content: ""; width: 8px; height: 8px; margin-left: auto;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg); transition: transform .15s ease-out;
}
.dz-more-btn[aria-expanded="true"]::after { transform: rotate(-135deg); }
.dz-more-btn:hover { color: var(--pf-black); }
.dz-more-body[hidden] { display: none; }
/* the retired two-step ladder leaves no trace: inside the disclosure both
   old steps simply show, stacked */
.dz-more-body .dz-step { grid-area: auto; }
.dz-more-body .dz-step[data-off] { visibility: visible; pointer-events: auto; }
/* the strip scrolls by its arrows — no scroll furniture underneath */
.dz-tpl-row { scrollbar-width: none; -ms-overflow-style: none; }
.dz-tpl-row::-webkit-scrollbar { display: none; height: 0; }


/* ── THE EXPAND CONTROL IS ITS ARROWS (Jon-ruled 2026-08-10) ───────────
   The word "EXPAND" was doing the work an icon does. Arrows only,
   understated, and still a real 40px target. */

/* ── THE MAP'S CLOSE CROSS SITS ON THE MAP (Jon-ruled 2026-08-10) ──────
   It was floating off the artefact. It belongs at the map's own top
   right, over the picture it closes. */
.lightbox .lbx-close {
  position: absolute; top: 10px; right: 10px; z-index: 3;
  width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center;
  background: var(--pf-white); border: 1px solid var(--pf-border); border-radius: 999px;
  box-shadow: 0 2px 10px rgba(0,0,0,.14);
}
.lightbox .lightbox-body { position: relative; }


/* ── ONE SHEET ON SCREEN (Jon's walk, 2026-08-10) ──────────────────────
   The shared product was showing TWO flyers in its design area: the cell
   preview's instance AND the card stage's own copy of the same sheet.
   The stage is the SOURCE now — parked off-screen exactly as the flyer's
   canonical frame is — and the gallery's preview is the one picture the
   customer sees. Nothing is deleted: the instance is cloned from it. */
.zone[data-zone="design"] .artefact-stage > .stage-content[data-pv="card"] {
  position: absolute; left: -99999px; top: 0; width: 460px; pointer-events: none;
}


/* ── THE BOOKING UI IS ONE CANVAS ABOVE THE FOLD (Jon-ruled 2026-08-10) ─
   Measured before: at 1440x900 the booking UI starts at y170, leaving
   730px above the fold, and the three sections stood 894 / 1047 / 820 —
   every one of them cut off. They are SIZED TO THE CANVAS now: each
   section takes the height that is actually there and scrolls its own
   content inside, with its header tab pinned so you always know which
   section you are in. DELIVERY fits entirely; DESIGN keeps its essentials
   and Generate at the top of its own scroll; CONFIRM fills what is left.
   Nothing overflows, and the page itself stops running past the screen. */
@media (min-width: 1200px) {
  /* EACH COLUMN TAKES ITS OWN HEIGHT (Jon-ruled 2026-08-15). The stretch
     above belonged to the sized-canvas era, when every section had a
     max-height and scrolled inside itself; those went (see the reset below,
     which is what actually governs now) and the stretch stayed behind on its
     own. With nothing to fill it, a column stretched to the tallest of the
     three and stood its content at the top: on Shared at 1440, SPEC held
     449px of content in a 1109px box — SIX HUNDRED AND SIXTY pixels of white
     beneath it, which reads exactly like a section that failed to paint.
     That is what Jon met at 1030 after a resize, and Chrome named it. */
  .deck-body { align-items: start; }
  .deck-body > .zone[data-zone] {
    max-height: calc(100vh - 190px);
    display: flex; flex-direction: column; min-height: 0;
    overflow: auto; overscroll-behavior: contain;
  }
  /* (the sticky header tab RETIRED with the internal scrollers it existed
     for — with nothing scrolling inside a section there is nothing to pin,
     and it was overlapping the guide bar at depth) */
  .deck-body > .zone[data-zone] > .zone-lab { margin-top: 0; }
  /* the expanded design window is its own canvas and keeps its own rules */
  }


/* ── THE NO-SCROLL LAW (Jon-ruled 2026-08-10) ──────────────────────────
   "Everything must be visible or usable above the fold without scrolling.
   Making a section internally scrollable is a defect, not a solution."
   So the internal scrollers introduced an hour earlier are RETIRED, and
   the content is made to fit instead.
   ON THE PHONE three open sections cannot share 844px, so the LIVE one
   shows its body and the other two stand as their header tabs — the same
   one-live-at-a-time the guidance law already states, made structural.
   Tapping a tab makes that section live, which opens it: no new gesture,
   no new control, nothing hidden that the customer cannot reach. */
@media (min-width: 1200px) {
  .deck-body > .zone[data-zone] { overflow: visible; max-height: none; }
}
/* A SECTION ONLY FOLDS IF IT HAS SOMETHING TO SAY FOLDED (Jon-ruled
   2026-08-15). "A section may collapse to a tab WITH ITS SUMMARY; it may
   never be an empty expanded column." A section the customer has not
   reached has no summary line, so folding it left a bare strip — on a
   1030 laptop, two of the three at once. It now keeps its body until it
   has a line to fold into, and the attribute is written by the summary's
   only writer, so the two can never disagree. Absent = unfolded: the
   first frame errs towards holding content. */
@media (max-width: 1199px) {
  .deck-body > .zone[data-zone][data-sum="1"]:not(.is-live) > :not(.zone-lab) { display: none !important; }
  .deck-body > .zone[data-zone][data-sum="1"]:not(.is-live) { padding-bottom: 0; }
  .deck-body > .zone[data-zone] > .zone-lab { margin-bottom: 0; }
  .deck-body > .zone[data-zone].is-live > .zone-lab { margin-bottom: var(--s-2); }
}


/* ── THE LEFTOVER STAGE IS PARKED (no-scroll law, 2026-08-10) ──────────
   Under the gallery ruling the customer's flyer is displayed in the FACE
   PREVIEWS. The old artefact stage still sat in the flow underneath them
   holding a canonical frame nobody sees and a designer pane that paints
   nothing there — 154px of the canvas spent on a source. Sources belong
   off-screen (this is the same treatment the canonical flyer frame has
   had since the door saga), so the stage is parked and the gallery is
   the picture. Nothing is deleted: every painter still reads from it. */
.zone[data-zone="design"] > .deck-preview > .artefact-stage {
  position: absolute; left: -99999px; top: 0; width: 460px; pointer-events: none;
}
/* the previews take the room the stage gave back */
@media (min-width: 1200px) {
  .dz-face-prev .pff-scaled { max-height: 236px; overflow: hidden; }
  .dz-tpl > label { margin-bottom: 2px; }
  .dz-essentials { gap: 6px; margin-bottom: 6px; }
  .dz-gallery { gap: 8px; margin-bottom: 8px; }
}


/* ── ROUND TWO OF THE FIT (no-scroll law) ─────────────────────────────
   Measured after round one at 1440x900 against a 754px canvas:
   DELIVERY 809 · DESIGN 835 · CONFIRM 670. What follows is SHRINK and
   MERGE, not scroll — the last 80-90px each of the two that still stood
   over the line. */
@media (min-width: 1200px) {
  /* DELIVERY: the steps keep their labels and controls, at tighter air */
  .deck .cfg-step { padding-block: 8px; }
  .deck .cfg-step .cfg-label { margin-bottom: 4px; font-size: 14px; }
  .deck .seg--stack button small { display: block; font-size: 11px; line-height: 1.25; }
  .deck .price-lines .pl { min-height: 22px; font-size: 14px; }
  .deck .price-lines .pl.total { font-size: 17px; }
  /* DESIGN: the picture stays the biggest thing, at the size the canvas
     can actually give it, and the strip's thumbs step down with it */
  .dz-face-prev .pff-scaled { max-height: 190px; }
  /* THE THUMBNAILS DO NOT GROW AT THE BREAKPOINT (Jon-ruled 2026-08-24).
     They were 44px below 1200 and 54px above it — bigger pictures in a
     NARROWER column, at exactly the pixel where the row wrapped, and the
     flyer paid for both. Held at 44px: 14px of the 156px step. (The 64->72px
     rule I deleted first was never the one that applied; this is.) */
  .dz-tpl-row > button > img { width: 44px; }
  .dz-tpl-row > button > span { max-width: 44px; font-size: 9px; }
  .dz-car-arw { width: 40px; height: 40px; }
  .dz-essentials textarea { min-height: 44px; }
  .zone[data-zone="design"] .field { margin-bottom: 3px; }
}


/* ── THE LAST THIRTY PIXELS (no-scroll law) ───────────────────────────
   After round two: DELIVERY 785 · DESIGN 761 · CONFIRM 664 against a
   754px canvas. This is the shave that puts the last two under the line,
   and it is air, not content: no control leaves, no label is dropped. */
@media (min-width: 1200px) {
  .deck .cfg-step { padding-block: 5px; }
  .deck .cfg-step .cfg-label { margin-bottom: 3px; font-size: 13px; }
  .deck .price-lines .pl { min-height: 20px; }
  .deck .price-lines { gap: 3px; margin-top: 8px; }
  .zone[data-zone="design"] .field { margin-bottom: 2px; }
  .dz-under-block .seg { min-height: 34px; }
  .dz-under { gap: 6px; margin-top: 6px; }
  .dz-more-btn { padding: 6px 2px; }
}


/* DELIVERY's last 21px: the week's explanatory hint reads as one compact
   line instead of two, and the booking button loses its extra top margin.
   Nothing is removed — the sentence is intact, at hint size. */
@media (min-width: 1200px) {
  .deck .cfg-step .hint { font-size: 11px; line-height: 1.3; margin-top: 1px; }
  .deck-rail .in-flow-cta, .deck-pane .in-flow-cta { margin-top: 8px !important; }
  .deck .per-letterbox { min-height: 16px; font-size: 12px; }
}


/* MERGED, not scrolled: the postcode and the homes tier are both short
   controls and they answer one question together — where, and how many.
   They share a row on the desktop canvas, which is a whole step of height
   back. Selected by the control they contain, so the rule cannot drift
   onto a different step if the order changes. */
@media (min-width: 1200px) {
  .deck-pane[data-pane="solo"] .cfg-step:has(#dk-postcode),
  .deck-pane[data-pane="solo"] .cfg-step:has(#dk-homes-l) {
    display: inline-block; width: calc(50% - 5px); vertical-align: top;
  }
  .deck-pane[data-pane="solo"] .cfg-step:has(#dk-postcode) { padding-right: 8px; }
  /* and the gallery gives back its last few, keeping the picture dominant */
  .dz-face-prev .pff-scaled { max-height: 168px; }
}


/* the desktop's last 47px: the picture keeps its dominance at a size the
   canvas can actually hold, and the gallery's own air comes in */
@media (min-width: 1200px) {
  .dz-face-prev .pff-scaled { max-height: 140px; }
  .dz-gallery { gap: 6px; margin-bottom: 6px; }
  .dz-face-block { gap: 2px; }
  .dz-essentials { gap: 5px; margin-bottom: 5px; }
}
/* SUPERSEDED, and removed rather than left to rot: "both faces show, the
   front first" was replaced by the TOGGLE (no-scroll wins on mobile). Its
   108px back box outlived it and was the reason the back preview broke its
   container — a box shorter than the painter's old budget floor. One face
   shows at a time now, so the back gets exactly the front's box. */
@media (max-width: 1199px) {
  .dz-gallery { grid-template-columns: 1fr; }
}


/* the phone canvas: the picture at the size one screen can hold beside
   its strip and the offer, and the artwork controls fold with the rest */
@media (max-width: 1199px) {
  .dz-face-prev .pff-scaled { max-height: 200px; overflow: hidden; }
  .dz-under { display: none; }
  .dz-more-body .dz-under { display: grid; }
}
/* desktop's last twelve */
@media (min-width: 1200px) {
  .deck-body > .zone[data-zone] { padding-bottom: 10px; }
  .dz-more { margin-top: 6px; }
}


/* THE PHONE CANVAS, FINAL SIZING (no-scroll law). 844px holds three
   section tabs, the live section's picture, its template strip, the offer
   and the press — and that is all it holds. The preview is the largest
   thing that leaves room for the rest; everything answered once is
   already folded away. */
@media (max-width: 1199px) {
  .dz-face-prev .pff-scaled { max-height: 140px; }
  .dz-tpl-row > button > img { width: 44px; }
  .dz-tpl-row > button > span { max-width: 44px; font-size: 9px; }
  .dz-car-arw { width: 40px; height: 40px; }
  .dz-tpl > label { font-size: 11px; margin-bottom: 2px; }
  .dz-essentials { gap: 5px; margin-bottom: 5px; }
  .dz-essentials textarea { min-height: 40px; }
  .dz-gallery { gap: 6px; margin-bottom: 6px; }
  .deck-body > .zone[data-zone] { padding-bottom: 8px; }
  .deck-body { gap: 10px; }
}
@media (min-width: 1200px) { .deck-body { gap: 14px; } .deck-body > .zone[data-zone] { padding-bottom: 8px; } }


/* THE LAST OF IT: desktop was 2px over, the phone 54. The picture takes
   the phone's share (140 -> 110) and its strip loses its label there —
   the row's own thumbnails say what it is — while desktop trims the
   deck's top padding. Nothing else leaves. */
@media (max-width: 1199px) {
  .dz-face-prev .pff-scaled { max-height: 110px; }
  .dz-gallery .dz-tpl > label { position: absolute; left: -9999px; }
  .dz-face-block { gap: 2px; }
  .dz-more { margin-top: 4px; }
}
.deck-body { padding-top: 6px; }


/* ── CONTAINMENT: NOTHING OVERFLOWS, NOTHING IS CLIPPED (Jon-ruled
   2026-08-10) ─────────────────────────────────────────────────────────
   THE PREVIEW IS SCALED, NOT CROPPED. My own cap clipped the flyer with
   overflow:hidden — the artefact must RESIZE to fit its area, exactly as
   the print templates already do. The host states a HEIGHT and the
   painter scales the whole face into it (that height is a budget it has
   always honoured), so the whole flyer is visible at rest. */
.dz-face-prev { overflow: visible; }
.dz-face-prev .pff-scaled { max-height: none; overflow: visible; }
/* every control in the three sections shrinks with its box */
.zone[data-zone] .seg button,
.zone[data-zone] .pfsh-shape,
.zone[data-zone] .pfsh-term { min-width: 0; max-width: 100%; }
.zone[data-zone] .seg button b,
.zone[data-zone] .seg button small,
.zone[data-zone] .pfsh-shape > * { min-width: 0; overflow-wrap: anywhere; }
@media (max-width: 420px) { .zone[data-zone] .seg button { font-size: 14px; padding: 9px 4px; } }


/* ── THE BRIEF ROW (Jon-ruled 2026-08-10) ─────────────────────────────
   Business name · Category · Generate on ONE line, at the top of the
   design area, on BOTH classes — the arrangement he liked on desktop,
   worked into the phone. It must not wrap into a stack, and every control
   in it meets the 44px finger floor. The offer sits directly beneath, in
   the same compact group. */
.dz-ess-row {
  display: grid; grid-template-columns: 1fr 1fr auto; gap: 6px; align-items: end;
}
.dz-ess-row > .field { margin-bottom: 0; min-width: 0; }
.dz-ess-row > .field > label { font-size: 11px; margin-bottom: 2px; white-space: nowrap; }
.dz-ess-row :is(input, .cat-pick) { min-width: 0; }
.dz-ess-row input { height: 44px; }
/* THE LOCKED GENERATE reads as locked and says what is still needed */
.dz-gen {
  min-height: 44px; padding: 0 12px; white-space: nowrap;
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
}
.dz-gen .dz-gen-why { font-size: 9px; font-weight: 700; opacity: .9; text-transform: none; letter-spacing: 0; }
.dz-gen .dz-gen-why:empty { display: none; }
.dz-gen.is-locked {
  background: var(--pf-grey-50); color: var(--pf-grey-600);
  border: 1px dashed var(--pf-seg-border); cursor: not-allowed;
}
.dz-gen.is-locked:hover { background: var(--pf-grey-50); }
@media (max-width: 420px) {
  .dz-ess-row { grid-template-columns: 1fr 1fr auto; gap: 4px; }
  .dz-ess-row > .field > label { font-size: 10px; }
  .dz-gen { padding: 0 8px; font-size: 13px; }
}


/* ── THE ZOOM (Jon-ruled 2026-08-10; supersedes the expand control) ────
   A face fills the canvas: centred, between the sticky chrome and the
   bottom margin, never scrolling and never clipping. Same page — no
   dialog, no backdrop, no route change — with the X at its top right.
   THE FLYER IS THE TRIGGER, so the faces say so to a finger. */
/* NO MAGNIFYING GLASS OVER A FACE THAT DOES NOT ZOOM (Jon's walk, 2026-08-23:
   "a magnifying glass appears on hover on desktop and does nothing"). It was
   the cursor, not a control: the face-click zoom door was retired when the
   labelled door landed — one door, one meaning — and this promise was left
   behind pointing at it. The face is an EDIT surface now, and says so. */
.dz-face-prev { cursor: default; }
.dz-face-prev #dz-regions [data-region] { cursor: text; }
.dz-zoom[hidden] { display: none; }
.dz-zoom {
  position: fixed; z-index: 70;
  top: calc(var(--pf-bar-h, 0px) + 56px); bottom: 12px;
  left: 50%; transform: translateX(-50%);
  /* EDIT SHOWS ONE FACE AT MAXIMUM SIZE (Jon-ruled 2026-08-25, SUPERSEDES the
     pair ruled on 2026-08-24). The 760px cap here was what the pair rule used
     to override; with one face it IS the size, and 760 was leaving the picture
     at 428x601 in a box that could hold 505x709. It is also the only shape that
     serves portrait and landscape alike: a landscape sheet needs 995 of width
     at this height, which two faces could never give it and 760 could not
     either. The width is the ceiling, not the picture — the picture is
     height-bound at 505x709 portrait, and the room left over is where the band
     editor stands while a band is being edited. */
  width: min(1240px, calc(100vw - 24px));
  display: flex; align-items: center; justify-content: center;
  background: var(--pf-white); border: 1px solid var(--pf-border);
  border-radius: var(--r-card); padding: 14px;
  box-shadow: 0 26px 60px rgba(0,0,0,.24), 0 6px 16px rgba(0,0,0,.10);
}
.dz-zoom-face { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.dz-zoom-face .pff-scaled { margin: 0 auto; }
.dz-zoom-x {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center;
  background: var(--pf-white); color: var(--pf-ink, #111);
  border: 1px solid var(--pf-border); border-radius: 999px; cursor: pointer;
}
.dz-zoom-x:hover { border-color: var(--pf-ink, #111); }
/* the zoomed face carries the edit layer, so the regions must sit over it */
.dz-zoom #dz-regions { z-index: 4; }
@media (max-width: 1199px) {
  /* ON THE PHONE THE ZOOM IS THE WORKING VIEW: it genuinely fills the
     screen below the chrome, and pinch-zoom is allowed inside it. */
  .dz-zoom { top: calc(var(--pf-bar-h, 0px) + 44px); bottom: 8px; width: calc(100vw - 12px); padding: 8px; touch-action: pinch-zoom; }
}
body.dz-zoomed { overflow: hidden; }


/* ── THE BAR CARRIES THE JOURNEY, WHOLE (Jon-ruled 2026-08-10) ─────────
   All three steps must be readable on a phone. With the sandbox line gone
   from the bar the width is there; the words wrap rather than clip, and
   the separators tighten so the three fit. */
#pf-guide { white-space: normal; text-align: center; }
.pfg-words { overflow: visible; text-overflow: clip; }
@media (max-width: 768px) {
  #pf-guide { font-size: 11px; padding: 7px 8px; gap: 6px; line-height: 1.35; }
  .pfg-sep { margin: 0 3px; }
}
.pf-sys-truth {
  margin: 0; padding: 4px var(--gutter); text-align: center;
  background: var(--pf-grey-50); color: var(--pf-grey-600);
  font-size: 11px; font-weight: 700; border-bottom: 1px solid var(--pf-border);
}
.pf-sys-truth[hidden] { display: none; }

/* ── THE SIGN-IN CARD (Jon-ruled 2026-08-10) ──────────────────────────
   The email capture is one deliberate card — centred, boxed, in the same
   register as the primary action — not loose form parts: the heading, the
   field, the button and the consent line composed together. */
.dz-si-card {
  display: block; max-width: 380px; margin: 10px auto; padding: 16px 14px;
  background: var(--pf-white); border: 1px solid var(--pf-border);
  border-radius: var(--r-card); box-shadow: var(--shadow-hover);
  text-align: center;
}
.dz-si-lead {
  display: block; font-weight: 800; font-size: 15px; color: var(--pf-black);
  margin-bottom: 8px; text-align: center;
}
.dz-si-row { display: flex; flex-direction: column; gap: 8px; }
.dz-si-row input {
  width: 100%; height: 44px; text-align: center;
  border: 1px solid var(--pf-border); border-radius: var(--r-btn); padding: 0 12px;
}
#dz-si-go {
  width: 100%; min-height: 48px; cursor: pointer;
  background: var(--pf-orange); color: #fff; border: 0; border-radius: var(--r-btn);
  font: 800 15px/1 Inter, system-ui, sans-serif;
}
#dz-si-go:hover { background: var(--pf-orange-dark); }
.dz-si-consent {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  margin-top: 8px; font-size: 12px; color: var(--pf-grey-600); text-align: center;
}
#dz-si-note { display: block; margin-top: 8px; text-align: center; font-size: 12.5px; font-weight: 700; }
#dz-si-note.dz-si-fail { color: var(--pf-attention); }


/* ── TILE ROWS ARE SIZED BY THEIR COLUMN, NOT THEIR CONTENT (Jon-ruled
   2026-08-10) ────────────────────────────────────────────────────────
   Measured at 1440: the Homes row was 158px wide and its content needed
   168 — three tiles squeezed to 47px each while "10,000" simply does not
   fit 47px at 15pt. It broke at every width, not just narrow ones,
   because the tiles were sized by their labels.
   The row is a sharing GRID now: every tile takes an equal share of the
   column, and when the column cannot hold them side by side they WRAP as
   a lawful group rather than burst. The label steps down within the
   register — never below the 13px the deck already uses — and the numbers
   are allowed to sit tight. The same rule covers the mode, space and term
   selectors: one family, one behaviour. */
.seg, .pfsh-shapes, .pfsh-terms {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(62px, 1fr));
  gap: 6px; align-items: stretch;
}
.seg > button, .pfsh-shape, .pfsh-term {
  min-width: 0; max-width: 100%; width: auto;
  overflow-wrap: normal; word-break: keep-all;
}
.seg > button { padding: 10px 4px; font-size: 14px; text-align: center; }
@media (max-width: 420px) { .seg > button { font-size: 13px; padding: 9px 3px; } }
/* the stacked mode selector keeps its two-line cards: one per row */
.seg--stack { grid-template-columns: 1fr; }
.seg--stack > button { text-align: left; padding: 10px 10px; }
/* a tile's own text never sets the row's width */
.seg > button, .pfsh-shape > *, .pfsh-term > * { min-width: 0; }


/* ── THE GENERATE BUTTON'S TWO STATES (Jon-ruled 2026-08-10) ──────────
   It is the AI doing the work, so the ✦ STAR rides in BOTH states.
   LOCKED IS NEVER GREY — a softer orange ground with orange text: it
   reads as "waiting for you", not dead, and its short line says what is
   still needed. MEASURED: the ratified attention orange on that ground is
   4.56:1 (the brand orange itself would be 3.11:1, which is why the text
   takes the darker register the deck already uses for orange type).
   READY is the full brand orange with the star in a gentle ORBIT —
   drawing the eye, never flashing — and the orbit settles the moment
   generation starts. Motion is opt-out by construction: it exists only
   inside prefers-reduced-motion: no-preference. */
.dz-gen { position: relative; gap: 3px; }
.dz-gen .dz-gen-star { display: inline-flex; line-height: 0; }
.dz-gen .dz-gen-t { font-weight: 800; letter-spacing: .01em; }
.dz-gen.is-locked {
  background: #FFE7E1; color: var(--pf-attention);
  border: 1px solid rgba(204, 42, 1, .35); cursor: not-allowed;
}
.dz-gen.is-locked:hover { background: #FFE7E1; }
.dz-gen.is-locked .dz-gen-star { color: var(--pf-attention); opacity: .9; }
.dz-gen:not(.is-locked) {
  background: var(--pf-orange); color: #fff; border: 0;
}
.dz-gen:not(.is-locked) .dz-gen-star { color: #fff; }
@media (prefers-reduced-motion: no-preference) {
  /* THE ORBIT: only ever on a button that is genuinely actionable */
  .dz-gen:not(.is-locked):not(.is-busy) .dz-gen-star {
    animation: pf-star-orbit 2.6s ease-in-out infinite;
  }
  @keyframes pf-star-orbit {
    0%   { transform: translate(0, 0) rotate(0deg); }
    25%  { transform: translate(2.5px, -2px) rotate(90deg); }
    50%  { transform: translate(0, -3px) rotate(180deg); }
    75%  { transform: translate(-2.5px, -2px) rotate(270deg); }
    100% { transform: translate(0, 0) rotate(360deg); }
  }
}
.dz-gen.is-busy .dz-gen-star { animation: none; opacity: .8; }


/* ── THE TOP CLUSTER SPEAKS ONE LANGUAGE (Jon-ruled 2026-08-10) ───────
   Generate wears the AI-design button's own shape — the deck's segment
   language — rather than a new one, with upload beside it as the quiet
   secondary and the orientation with them. The RULED states are unchanged
   and simply expressed in that language: locked is the softer orange with
   orange text and its reason (4.56:1 measured), ready is full brand orange
   with the star in its orbit, and the ✦ mark rides both. */
.dz-cluster { display: grid; gap: 6px; margin: 8px 0 10px; }
.dz-cluster .dz-under-block { display: grid; grid-template-columns: 1fr auto; gap: 6px; align-items: center; }
.dz-cluster .seg { gap: 6px; }
.dz-cluster .upload-note:empty { display: none; }
/* Generate in the segment language: same radius, same weight, same height */
.dz-gen {
  width: 100%; min-height: 44px; border-radius: var(--r-btn);
  font: 800 15px/1 Inter, system-ui, sans-serif;
  display: inline-flex; flex-direction: row; align-items: center; justify-content: center; gap: 7px;
  border: 1px solid transparent;
}
.dz-gen .dz-gen-why {
  font: 700 11px/1 Inter, system-ui, sans-serif; opacity: .95;
  text-transform: none; letter-spacing: 0;
}
.dz-gen .dz-gen-why:empty { display: none; }
.dz-gen .dz-gen-why::before { content: "— "; }


/* ── THE THREE TILES HOLD ONE ROW AT EVERY WIDTH (Jon-ruled 2026-08-10) ─
   auto-fit let "10,000" push a tile onto a second line in the delivery
   panel — tidy in theory, messy in fact. The homes row is THREE equal
   columns, always, and the label lives within the row's own scale: the
   number shrinks with the column rather than breaking the row. The same
   discipline holds the space and term selectors. */
/* ── THE HOMES TILES FIT THEIR OWN ROW (Jon, third report) ────────────
   The label was sized off the VIEWPORT (2.6vw) while the tile is sized by
   its container — and on a wide desktop those pull opposite ways: the
   three-column layout makes the rail NARROWER as the screen gets wider,
   so at 1440 the row is 158px, each tile 49px, and "10,000" at its 15px
   maximum needs 54px. The bigger the screen, the worse it got, which is
   why it kept surviving fixes aimed at small screens.
   The label now scales from THE ROW ITSELF, so a tile that shrinks takes
   its text down with it and the relationship cannot invert again. */
#dk-homes { grid-template-columns: repeat(3, minmax(0, 1fr)); container-type: inline-size; }
#dk-homes > button {
  font-size: clamp(9px, 7cqi, 15px);
  padding: 10px 2px; white-space: nowrap; min-width: 0; overflow: hidden;
}
.pfsh-shapes { grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); }
.pfsh-shape { white-space: normal; }
.pfsh-terms { grid-template-columns: 1fr; }

/* ── NO GREY SEPARATOR LINES IN THE BOOKING CHROME (Jon-ruled
   2026-08-10) ── The fine-grey separator is the FLYER'S idiom; in the
   delivery panel it just made the section look ruled and messy. The
   section's own boxes and spacing do the separating. */
.deck .cfg-step { border-bottom: 0; }
.deck .cfg-step + .cfg-step { border-top: 0; }
.zone[data-zone] .deck-pane hr, .zone[data-zone] .cfg-step hr { display: none; }


/* the bar is one line, centred, and it never wraps — the short voice is
   chosen by width, not by the browser breaking a word */
#pf-guide { white-space: nowrap; justify-content: center; }
.pfg-words { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pfg-ai[hidden] { display: none; }
/* THE OLD ORIENTATION RULES ARE GONE, and they are worth a line because they
   are the fifth "correct but inert" in this file — from the other side. An ID
   selector beats any number of classes, so `#dz-orient button { display:
   inline-flex }` quietly outranked the control system's own resting rule and
   left LANDSCAPE standing in a 44px strip beside PORTRAIT while every other
   two-state control stood its other half down. The system owns these boxes
   now; nothing addresses them by id. */


/* ── ONE FACE AT A TIME ON THE PHONE (Jon-ruled 2026-08-10) ────────────
   No-scroll wins: at 390 the front shows and the back is one tap away.
   The blocks both stay in the DOM — only their visibility changes — so
   the switch cannot lose an edit. Desktop shows both and never sees the
   toggle. */
.dz-facesw { display: none; }
@media (max-width: 1199px) {
  .dz-facesw {
    display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 6px;
  }
  .dz-facesw button {
    min-height: 40px; cursor: pointer; border-radius: var(--r-btn);
    border: 1px solid var(--pf-border); background: var(--pf-white);
    font: 700 13px/1 Inter, system-ui, sans-serif; color: var(--pf-grey-600);
  }
  .dz-facesw button[aria-pressed="true"] {
    border-color: var(--pf-orange); background: var(--pf-orange-tint); color: var(--pf-attention);
  }
  .dz-gallery[data-face-shown="front"] .dz-face-block[data-face="back"],
  .dz-gallery[data-face-shown="back"] .dz-face-block[data-face="front"] { display: none; }
}

/* ── THE NO-SCROLL LAW, PAID FOR IN PIXELS (2026-08-10) ────────────────
   Measured, not guessed: at 390 the three sections had 633px of canvas and
   were spending 946. Jon's law allows four answers — move onto the
   artefact, fold into a disclosure, shrink or merge, or cut — and forbids
   the fifth (scroll it). These are the SHRINKS, each one taken against a
   measured cost rather than a hunch. What could not be paid for this way
   is written up for his ruling instead of quietly scrolled. */
@media (max-width: 1199px) {

  /* THE MAP SHRINKS ON MOBILE — this is the V1 card's ratified mobile
     behaviour ("map shrinking, everything visible"), not a new idea.
     16:10 costs 224px of picture for a coverage hint. */
  /* .map-slot pins its own height (clamp(300px, 24vw, 420px)), so the
     aspect-ratio underneath it never got a say — the slot is what to
     change. 300px of picture to answer "roughly where does this go" is
     more than the question needs on a phone. */
  .deck-rail .map-slot, #dk-map-slot { height: 176px; }
  #dk-map-cap { font-size: 12px; }

  /* the extras disclosure is a line of text until it is opened; it does
     not need a rule above it and a block of padding to say so */
  .dz-more { margin-top: 6px; border-top: 0; }
  .dz-more-btn { padding: 6px 0; }
}

/* (the accessible name uses the existing .visually-hidden — one vocabulary) */
/* ORIENTATION: two shapes, one row — never a stacked pair of words */
.seg--orient { grid-template-columns: 1fr 1fr !important; }
.seg--orient button { display: flex; align-items: center; justify-content: center; min-height: 40px; }
.seg--orient svg { width: 22px; height: 22px; display: block; }

/* THE LAST 30px, FOUND RATHER THAN GUESSED. With CONFIRM live the stack
   measured 874 against an 844 fold, and desktop 930 against 900 — the same
   30px both times, which is what a gap looks like when it is charged three
   times over. The three sections are ONE control surface; they do not each
   need a full gutter between them, and a collapsed tab does not need the
   padding of an open section. */
.deck-body { gap: var(--s-2); }
.zone:not(.is-live) { padding-bottom: 0; }
.zone:not(.is-live) > .zone-lab { margin-bottom: 0; }
/* AND A GUTTER BETWEEN TWO FOLDED TABS IS A GUTTER SPENT ON NOTHING (measured
   2026-08-15). Where one section is open and the other two stand as tabs, the
   16px gutter is charged twice over for a boundary between two 43px strips.
   10px still reads as a boundary and gives 12px back to the section that is
   actually being used — the fold was 59px short at 390 and every part of the
   stack is measured, so this is where the slack honestly is. */
@media (max-width: 1199px) { .deck-body { gap: 10px; } }
/* and on a handset, where the fold is tightest and the boundary is between
   two 43px strips, 8px still reads as a boundary — CONFIRM was three pixels
   over, which is a gutter and not a section */
@media (max-width: 767px) { .deck-body { gap: 8px; } }


/* ── THE FACE TOGGLE, QUIETENED AND RE-SITED (Jon, 2026-08-10) ─────────
   It sits under the templates and is understated: a small control that
   says which side you are looking at, not a pair of buttons competing
   with the artefact above them. */
@media (max-width: 1199px) {
  .dz-facesw {
    display: flex; justify-content: center; gap: 4px;
    margin: 4px 0 0; padding: 0;
  }
  .dz-facesw button {
    min-height: 28px; padding: 4px 12px; border: 0; background: none;
    font: 600 11px/1 Inter, system-ui, sans-serif; color: var(--pf-grey-600);
    border-bottom: 2px solid transparent; border-radius: 0; cursor: pointer;
  }
  .dz-facesw button[aria-pressed="true"] {
    color: var(--pf-black); border-bottom-color: var(--pf-orange); background: none;
  }
}

/* ── THE DESIGN COLUMN'S DEAD SPACE (Jon, 2026-08-10) ──────────────────
   He named the offer→Generate gap; measuring it found the cause and two
   more of the same fault. #dz-narr is the generation narrator: 26px of
   EMPTY element at rest, sitting exactly where he pointed. It reserved
   that space to avoid a shift when narration appears — but paying 26px of
   fold on every visit to save one small shift during a generation the
   customer is already watching is the wrong trade under the no-scroll law.
   The rest is margins charged twice where two blocks meet. */
#dz-narr:empty { display: none; }
@media (max-width: 1199px) {
  .dz-cluster { margin-top: 4px; margin-bottom: 6px; }
  .dz-essentials { margin-bottom: 4px; }
  .dz-essentials .field { margin-bottom: 6px; }
  .dz-essentials .field > label { margin-bottom: 2px; }
}


/* THE CAROUSEL ARROWS MEET THE FINGER FLOOR. Jon set 44px; these were
   40 — under his own ruling, and an arrow you have to aim at is an arrow
   that reads as broken. */
.dz-car-arw { width: 44px; height: 44px; }

/* THE REFUSAL, WHERE THE FINGER IS (Jon, 2026-08-10). A console warning
   tells a developer nothing the customer can act on — "I tapped it and
   nothing happened" is the whole complaint, so a face that declines says
   why on screen. */
.dz-facenote {
  margin: 4px 0 0; font: 600 11px/1.3 Inter, system-ui, sans-serif;
  color: var(--pf-attention); text-align: center;
}
.dz-facenote[hidden] { display: none; }

/* THE FINISHED SECTION'S ONE LINE (Jon, 2026-08-11). A folded section used
   to say nothing about what it settled, so checking your own order meant
   reopening it. The line sits in the tab itself, so it costs no extra
   block — and it is derived from the controls at render time, never
   remembered, so it cannot describe a state the page has left. */
.zone-sum {
  margin-left: auto; font: 600 11px/1.2 Inter, system-ui, sans-serif;
  color: rgba(255, 255, 255, .78); text-align: right;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 55%;
}
.zone.is-live .zone-sum { display: none; }

/* THE SETTLED ROW (Jon, 2026-08-11). Delivery week and mode arrive with
   sensible defaults and were spending ~230px of a 527px canvas restating
   choices nobody had to make. One line, its own live values, 44px so a
   finger can always reopen it. */
.dk-settled {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; min-height: 44px; padding: 8px 10px; cursor: pointer;
  border: 1px solid var(--pf-border); border-radius: var(--r-btn);
  background: var(--pf-white); text-align: left;
  font: 600 13px/1.25 Inter, system-ui, sans-serif; color: var(--pf-ink, #111);
}
.dk-settled-v { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dk-settled-a { flex: none; font-weight: 700; color: var(--pf-attention); font-size: 12px; }
.dk-settled[aria-expanded="true"] { border-color: var(--pf-orange); }
.dk-settled[aria-expanded="true"] .dk-settled-a::after { content: " ▴"; }
.dk-settled[aria-expanded="false"] .dk-settled-a::after { content: " ▾"; }

/* the flyer-style card compacts to its select on the phone — a card block
   for a one-of-three choice is a card block the fold cannot afford */
@media (max-width: 1199px) {
  .pfsh-railcard.pfly-style { padding: 8px; }
  .pfsh-railcard.pfly-style h3, .pfsh-railcard.pfly-style h4 { font-size: 12px; margin: 0 0 4px; }
  .pfsh-railcard.pfly-style .seg--stack { grid-template-columns: 1fr 1fr; }
  .pfsh-railcard.pfly-style .seg--stack button { min-height: 44px; padding: 6px; }
  .pfsh-railcard.pfly-style .seg--stack button small { display: none; }
}

/* ── THE LAST PIXELS, TAKEN FROM WHITESPACE ONLY (Jon, 2026-08-11) ─────
   14px, 29px and 30px, out of padding and margins — no content removed,
   nothing ruled cut. Taken from the LOOSEST gaps first, measured rather
   than guessed:
     the tab-to-body gap was 16px in every section — the single loosest
     gap on the phone, and a section's own header does not need a full
     gutter beneath it when the whole section is the thing being read;
     CONFIRM carried a 22px gap above its call to action and a 16px one
     above its prices — both blocks in one column, both charged as if
     they were separate regions.
   THE TWO TIERS SURVIVE: rows inside a list stay at 4-6px so they still
   read as one list, and the gaps BETWEEN blocks stay visibly larger. The
   hierarchy is compressed, not flattened. */
@media (max-width: 1199px) {
  .deck-body > .zone[data-zone].is-live > .zone-lab { margin-bottom: 6px; }
  .deck-body > .zone[data-zone] { padding-bottom: 4px; }

  /* YOUR ORDER: the flyer card and the steps below it are one list */
  .deck-pane.active .pfsh-railcard.pfly-style { margin-bottom: 6px; }

  /* DESIGN: its blocks sit closer without merging into each other */
  .dz-cluster { margin-top: 4px; margin-bottom: 4px; }
  .dz-gallery { margin-bottom: 4px; }
  .dz-facesw { margin-top: 2px; }
  .dz-more { margin-top: 2px; }

  /* CONFIRM: the price block and the call to action are one column, not
     three regions that each need their own air */
  .rail-price .per-letterbox { margin-bottom: 8px; }
  #dk-pricing-slot { margin-top: 8px; }
  #dk-meta { margin-top: 4px; }
}

/* ── THE LAST OF THE LAST (measured, 2026-08-11) ──────────────────────
   CONFIRM's worst gap went UP after the first trim, which is what a
   guess looks like when it lands next to the real cause rather than on
   it. Measured: .per-letterbox is EMPTY — zero height — and still
   charging 8px of margin, and the call to action adds 16px of its own.
   An empty element paying rent is the same fault as the generation
   narrator that sat at 26px saying nothing. */
@media (max-width: 1199px) {
  /* CORRECTION (same day): I trimmed this on a wrong diagnosis. I read
     .per-letterbox at zero height and called it an empty element paying
     rent — it is not empty, it says "≈ 2.0p per letterbox", and it only
     measured zero because I read it while CONFIRM was closed. Measuring a
     closed section and reporting the answer as a defect is the mistake
     this session keeps finding; here I made it myself.
     The real remaining cost is three BLOCK GAPS inside one rail: the rail
     is a single column — map, meta, prices — and each junction was
     charging a full block gutter. */
  .rail-price .price-block, .rail-price .dk-card-cta { margin-top: 8px; }
  .deck-rail #dk-map-slot { margin-top: 4px; }
  /* the section's own floor: its last child already carries spacing */
  .deck-body > .zone[data-zone] { padding-bottom: 0; }
  /* DESIGN's remaining seven: the cluster's inner gap and the gallery's
     foot, both between-block spacings already at the low end */
  .dz-cluster .dz-under-block { margin-top: 3px; }
  .dz-gallery { margin-bottom: 2px; }
  /* DESIGN's last four, from the two gaps still at the loose end */
  .deck-body > .zone[data-zone].is-live > .zone-lab { margin-bottom: 4px; }
  /* CONFIRM's rail is one column: its junctions do not each need a gutter */
  #dk-meta { margin-top: 4px; }
  #dk-pricing-slot { margin-top: 4px; }
}

/* THE FINAL TWO AND FOUR (measured, 2026-08-11). DESIGN sat 2px over and
   CONFIRM 4px, on a page proven furnished and settled. Taken from the two
   loosest gaps still standing — the design header's foot, and the rail's
   two internal junctions, which belong to one column. */
@media (max-width: 1199px) {
  .deck-body > .zone[data-zone="design"].is-live > .zone-lab { margin-bottom: 2px; }
  #dk-meta { margin-top: 2px; }
  #dk-pricing-slot { margin-top: 2px; }
}

/* ── "BOOK MY DROP" GETS ITS OWN AIR BACK (Jon, 2026-08-11) ───────────
   Measured at 1px of clearance at 390 and 1024: my own whitespace trim
   zeroed the sections' floor and this button was the thing standing on
   it. It is the one control on the page that must never look accidental,
   so it gets a floor of its own rather than sharing the section's.
   PAID FOR, not added: the map gives back the same 12px it is asked for,
   so CONFIRM still meets the no-scroll law. A coverage hint can afford
   twelve pixels; the button that takes the money cannot. */
@media (max-width: 1199px) {
  .zone[data-zone="confirm"] #dk-solo-cta,
  .zone[data-zone="confirm"] .dk-card-cta,
  .zone[data-zone="confirm"] .btn-primary { margin-bottom: 12px; }
  .deck-rail .map-slot, #dk-map-slot { height: 164px; }
}
@media (min-width: 1200px) {
  .zone[data-zone="confirm"] #dk-solo-cta,
  .zone[data-zone="confirm"] .dk-card-cta { margin-bottom: 12px; }
}

/* the field a refusal points at, so the eye lands where the finger must go */
.dz-needs {
  outline: 2px solid var(--pf-orange); outline-offset: 2px;
  background: var(--pf-orange-tint);
}

/* the back's own Generate — same language as the front's, on the face it makes */
.dz-back-gen { width: 100%; margin-top: 6px; }
.dz-back-gen[hidden] { display: none; }
.dz-back-gen .dz-gen-why { font-weight: 600; opacity: .9; }

/* THE REFUSAL BESIDE THE BUTTON THAT REFUSED (Jon, 2026-08-11) */
.dk-cta-note {
  margin: 8px 0 0; font: 600 12px/1.4 Inter, system-ui, sans-serif;
  color: var(--pf-attention); text-align: center;
}
.dk-cta-note[hidden] { display: none; }

/* GENERATE SHOWS ITS WORK. The mint is 55-103 seconds; the button carries a
   fill that tracks the real elapsed time against that measured range, and
   its label speaks the engine's own current stage. It holds at 96% rather
   than claiming to finish before the work has. */
.dz-gen.is-working { position: relative; overflow: hidden; cursor: progress; }
.dz-gen.is-working::before {
  content: ""; position: absolute; inset: 0 auto 0 0;
  width: var(--pf-progress, 0%);
  background: rgba(255, 255, 255, .22);
  transition: width .7s linear;
  pointer-events: none;
}
.dz-gen.is-working .dz-gen-t { position: relative; z-index: 1; }
@media (prefers-reduced-motion: no-preference) {
  .dz-gen.is-working .dz-gen-star { animation: pf-gen-spin 1.6s linear infinite; transform-origin: 50% 50%; }
  @keyframes pf-gen-spin { to { transform: rotate(360deg); } }
}

/* THE CHECKOUT CAPTURE (Jon ruled 2026-08-11): two fields on one row, so
   the no-scroll law survives a surface that now asks for more. */
.dk-capture { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 8px; }
.dk-capture .field { margin: 0; }
.dk-capture label { display: block; font-size: 11px; font-weight: 700; margin-bottom: 2px; color: var(--pf-grey-600); }
.dk-capture input { width: 100%; min-height: 40px; }
@media (max-width: 420px) { .dk-capture { grid-template-columns: 1fr; } }
.dk-billing {
  margin: 6px 0 0; font: 600 11px/1.35 Inter, system-ui, sans-serif;
  color: var(--pf-grey-600);
}
.dk-billing[hidden] { display: none; }

/* ── MULTI-POSTCODE ORDER LINES (Jon-ruled 2026-08-12) ───────────────────
   Each stacked district is its own removable line carrying its own homes,
   price and channel verdict. Understated on purpose: this is the ORDER
   column, not a feature to admire — it has to read as a list of what the
   customer is buying, and the footer total must be the visible sum of the
   lines they can see. */
.dk-lines:empty { display: none; }
.dk-line {
  display: grid; grid-template-columns: 1fr auto auto; align-items: center;
  gap: 8px; padding: 8px 0; border-top: 1px solid var(--pf-border);
}
.dk-line-main { min-width: 0; }
.dk-line-pc { font-weight: 700; font-size: 13px; letter-spacing: .01em; }
.dk-line-sub { font-size: 11px; color: var(--pf-grey-600); line-height: 1.4; }
/* --pf-ink HAS NEVER EXISTED. It was caught once on the AI bar (2026-08-12,
   see the note further down) and fixed only at that one call site, so this
   one went on silently INHERITING --pf-grey-600 from .dk-line-sub — the
   remove control reading as body text rather than as ink. An undefined custom
   property does not error: the declaration is dropped and the element
   inherits, which is why three of these survived a fortnight. */
.dk-line-sub .dk-line-rm { color: var(--pf-black, #1A1A1A); }
.dk-line-homes {
  font-size: 12px; padding: 4px 6px; border: 1px solid var(--pf-border);
  border-radius: var(--r-btn); background: var(--pf-white); color: var(--pf-black);
}
.dk-line-price { font-size: 13px; font-weight: 700; white-space: nowrap; }
.dk-line-x {
  width: 28px; height: 28px; line-height: 1; border: 1px solid var(--pf-border);
  border-radius: var(--r-btn); background: var(--pf-white); color: var(--pf-grey-600);
  font-size: 15px; cursor: pointer; flex: 0 0 auto;
}
.dk-line-x:hover, .dk-line-x:focus-visible { border-color: var(--pf-orange); color: var(--pf-orange); }
.dk-addpc {
  margin-top: 8px; width: 100%; padding: 9px 10px; font: 700 12px/1.2 Inter, system-ui, sans-serif;
  letter-spacing: .02em; background: var(--pf-white); color: var(--pf-black);
  border: 1px dashed var(--pf-border); border-radius: var(--r-btn); cursor: pointer;
}
.dk-addpc:hover, .dk-addpc:focus-visible { border-color: var(--pf-orange); color: var(--pf-orange); border-style: solid; }
.dk-qp { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; }
.dk-qp-lab { font-size: 11px; color: var(--pf-grey-600); }
.dk-qp-list { display: flex; flex-wrap: wrap; gap: 6px; }
.dk-qp button {
  font: 700 11px/1 Inter, system-ui, sans-serif; padding: 6px 8px;
  border: 1px solid var(--pf-border); border-radius: var(--r-btn);
  background: var(--pf-white); color: var(--pf-black); cursor: pointer;
}
.dk-qp button:hover, .dk-qp button:focus-visible { border-color: var(--pf-orange); color: var(--pf-orange); }
.dk-qp button[disabled] { opacity: .45; cursor: default; }

/* ── THE DESIGN WINDOW (Jon-ruled 2026-08-12) ────────────────────────────
   One structure, two expressions. The gallery already stacks each face
   above its own carousel and goes side by side at >=980px, so the ruling's
   shape needed the pieces around it, not a new grid. */
/* PRIMARY CONTROLS: one row that wraps rather than a stack that scrolls.
   Every child keeps the 44px finger floor — the no-scroll law buys space by
   folding what is settled, never by shrinking what a thumb must hit. */
.dz-primary {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: 10px 0 0;
}
.dz-primary > * { min-height: 44px; }
.dz-primary .seg { flex: 1 1 auto; min-width: 0; }
.dz-primary .seg--orient, .dz-primary .dz-facesw { flex: 0 0 auto; }
.dz-reset {
  flex: 0 0 auto; min-height: 44px; padding: 0 14px;
  font: 700 12px/1 Inter, system-ui, sans-serif; letter-spacing: .02em;
  background: var(--pf-white); color: var(--pf-black);
  border: 1px solid var(--pf-border); border-radius: var(--r-btn); cursor: pointer;
}
.dz-reset:hover, .dz-reset:focus-visible { border-color: var(--pf-orange); color: var(--pf-orange); }
/* THE AI DESIGNER GLISTENS. It is the one door to a fresh mint now, so it is
   allowed to catch the eye — a slow sheen, not a flash. It respects
   prefers-reduced-motion, because "glistening" is not worth a headache. */
.dz-ai { margin-top: 12px; }
.dz-ai-btn {
  position: relative; overflow: hidden; width: 100%; min-height: 48px;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font: 800 14px/1 Inter, system-ui, sans-serif; letter-spacing: .02em;
  /* was var(--pf-ink) — a token that does not exist, so this declaration was
     dropped and the button had no ground. The 2026-08-12 note below explains
     the fix; this is the original rule corrected at source so the dead token
     is gone from the file rather than merely overridden. */
  color: #fff; background: var(--pf-black, #1A1A1A); border: 0; border-radius: var(--r-btn);
  cursor: pointer;
}
.dz-ai-btn .dz-ai-star { color: var(--pf-orange); display: inline-flex; }
.dz-ai-btn::after {
  content: ""; position: absolute; inset: 0 auto 0 -60%; width: 60%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .28), transparent);
  transform: skewX(-18deg); animation: dz-sheen 4.5s ease-in-out infinite;
}
@keyframes dz-sheen { 0%, 62% { left: -60%; } 100% { left: 130%; } }
@media (prefers-reduced-motion: reduce) { .dz-ai-btn::after { animation: none; } }
.dz-ai-btn:hover, .dz-ai-btn:focus-visible { background: #000; }
.dz-ai-opts { margin-top: 8px; }
.dz-ai-opts[hidden] { display: none; }

/* THE FLOOR IS ON THE CONTROL, NOT ITS WRAPPER (2026-08-12). `.dz-primary > *`
   put 44px on the seg containers while the BUTTONS inside them measured
   28-42px — walked at 390 and 1440 and caught by measuring every control
   rather than the row. Jon's mobile ruling names the 44px floor explicitly,
   and a floor that lands on a wrapper is not a floor a thumb can feel. */
.dz-primary button,
.dz-primary select,
.dz-primary .seg > button,
.dz-primary .dz-facesw button {
  min-height: 44px;
}
.dz-primary .seg--orient > button { min-width: 44px; }

/* THE TEMPLATE SCROLLER IS PERMANENT (Jon 2026-08-12). Its collapse rules
   are gone, not overridden — nothing can hide it after a pick. The chosen
   look is marked on the button itself, so the strip stays legible while
   the customer keeps comparing. */
.dz-tpl-row button[aria-pressed="true"] { outline: 2px solid var(--pf-orange); outline-offset: 2px; }

/* ── THE MONEY BUTTON GETS CLEARANCE ABOVE IT TOO (Jon-ruled 2026-08-12) ──
   Measured on the deployed build: 22px above at 390 and 1024, and ZERO at
   1440 — flush against the name/email capture, so it read as the last item
   in the field group rather than a distinct action. It already had a floor
   below it from the whitespace trim; this is the same discipline applied
   upward.
   PAID FOR FROM THE LOOSEST GAP, not from the fields and not from the
   button: the capture block's own bottom margin is where the slack was, so
   the clearance is added on the button and taken back there. The fields'
   internal spacing is untouched, and CONFIRM keeps its no-scroll budget. */
.zone[data-zone="confirm"] #dk-solo-cta { margin-top: 18px; }
.zone[data-zone="confirm"] #dk-capture { margin-bottom: 0; }
@media (max-width: 1199px) {
  /* at these widths the 22px sibling gap is the loosest thing in the
     section, so the extra clearance comes out of it and the section's
     height is unchanged */
  .zone[data-zone="confirm"] #dk-capture { margin-bottom: -8px; }
}

/* AT DESKTOP THE BUTTON'S OWN MARGIN DID NOT SEPARATE IT (2026-08-12).
   margin-top:18px produced 14px of clearance at 390 and 1024 and ZERO at
   1440 — the desktop rail lays the section out so the button's margin buys
   nothing. Measured, not assumed. The gap is put on the capture block
   instead, which works in either context, and CONFIRM has the room for it:
   789px against a 900px fold. */
@media (min-width: 1200px) {
  .zone[data-zone="confirm"] #dk-capture { margin-bottom: 18px; }
}

/* ── THE CONTROL ROW READS AS ONE ROW, AND PAYS FOR THE FLYER ────────────
   Measured at 390: the primary row stood at 196px — four controls each
   taking a full line — and the stack ran 133px PAST the fold before the
   flyers had any chance to grow. Jon's sizing ruling says the budget comes
   from spacing and the control row, never from the flyer, so this is where
   it comes from.
   Upload takes the full first line because its label is the long one; the
   three compact controls share the second. That is a deliberate two-line
   arrangement, not parts pushed together — and every control keeps the 44px
   floor rather than being shrunk to fit. */
.dz-primary { gap: 6px; margin-top: 8px; }
.dz-primary > #dk-solo-art { flex: 1 1 100%; }
}

.dz-primary > #dz-facesw { flex: 1 1 auto; }
.dz-primary > #dk-solo-orient { flex: 0 0 auto; }
.dz-primary > .dz-reset { flex: 0 0 auto; }
.dz-primary .upload-note:empty { display: none; }
@media (min-width: 980px) {
  /* desktop has the width for a single line */
  .dz-primary > #dk-solo-art { flex: 1 1 auto; }
}
/* the carousel is furniture, not a feature — it keeps its arrows and its
   scroll, at a height that leaves the flyer the room */
.dz-ai { margin-top: 8px; }

/* THE ROW IS THE UNDER-BLOCK, NOT ITS WRAPPER (2026-08-12). #dz-primary
   holds ONE child — .dz-under-block — which is what actually contains
   Upload, front/back, orientation and Reset. Styling the wrapper as a flex
   row therefore did nothing: the block stayed a 149px-wide column stacking
   four 44px controls into 196px, and the DESIGN stack ran 127px past the
   fold. Same shape as the finger-floor miss earlier today — the rule landed
   on the container above the things it was meant to govern. */
.dz-primary { display: block; }
.dz-primary .dz-under-block {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: 100%;
}
.dz-primary .dz-under-block > #dk-solo-art { flex: 1 1 100%; }
.dz-primary .dz-under-block > #dz-facesw { flex: 1 1 auto; }
.dz-primary .dz-under-block > #dk-solo-orient,
.dz-primary .dz-under-block > .dz-reset { flex: 0 0 auto; }
.dz-primary .dz-under-block > .upload-note:empty { display: none; }
@media (min-width: 980px) {
  .dz-primary .dz-under-block > #dk-solo-art { flex: 1 1 auto; }
}

/* ONE LINE WITH THE ZOOM DOOR, BOTH PRODUCTS (Jon-ruled 2026-08-23). The rule
   above gave the upload the whole line below 980px and named only the SOLO
   seg, so the card row was never governed at all — and "View full size" landed
   under the row on three of the four surfaces. The upload takes the slack; the
   three compact controls hold their size. */
.dz-primary .dz-under-block > #dk-solo-art,
.dz-primary .dz-under-block > #dk-card-art { flex: 1 1 148px; min-width: 0; }
.dz-primary .dz-under-block > .dz-full,
.dz-primary .dz-under-block > .dz-tick { flex: 0 0 auto; }
@media (max-width: 430px) {
  .dz-primary .dz-under-block > #dk-solo-art,
  .dz-primary .dz-under-block > #dk-card-art { flex: 1 1 84px; }
}

/* THE AI BAR HAD NO GROUND (2026-08-12). Measured on the deployed page:
   background rgba(0,0,0,0) with white text — invisible. The fill was written
   as var(--pf-ink), and that token does not exist, so the declaration was
   invalid and dropped, leaving white on nothing. Not a colour choice: a rule
   that never landed. It is the one control meant to catch the eye, so it is
   a filled primary with a real fallback colour, and the sheen now has a
   ground to travel across. */
.dz-ai-btn {
  background: var(--pf-black, #1A1A1A) !important;
  color: #fff !important;
}
.dz-ai-btn:hover, .dz-ai-btn:focus-visible { background: #000 !important; }

/* ── SPEND THE FOLD BUDGET ON THE FLYERS (Jon-ruled 2026-08-12) ──────────
   "Enlarge the faces to the biggest size that still keeps everything above
   the fold... if the maths says something has to give, take it from spacing
   and the control row's height, never from the flyer."
   Measured before: 390 was 27px OVER with faces at 308x148; 1366x768 had
   102px spare with faces at 206x158 each. The control row was already cut
   from 196px to 96/44. What is left comes from the gaps. */
.dz-gallery { gap: 8px; margin-bottom: 8px; }
.dz-face-block { gap: 2px; }
.dz-tpl { margin: 0; }
.dz-car { margin: 0; }
.dz-primary { margin-top: 6px; }
.dz-ai { margin-top: 6px; }
/* THE FACE TAKES THE ROOM THAT FREES UP. The painter treats the host's
   height as its budget and fits the artefact inside it, so a taller host is
   a bigger flyer — the documented hazard is a host too SHORT, which is what
   shrank a 426px face to 86px once. These heights are chosen from the
   measured headroom, not guessed. */

/* AT 390 THE STACK WAS ALREADY OVER THE FOLD BEFORE THE FACE GREW.
   Measured: -27px with the face at 148, -46px once it reached 168. The
   ruling is the biggest face that STILL KEEPS everything above the fold, so
   on a phone the face waits while the chrome pays first. The carousel is
   "low-height with arrows" by the mobile ruling, so that is where the rest
   comes from — the thumbs keep their 44px touch target through the strip's
   own scrolling, the strip simply shows less of each. */
@media (max-width: 979px) {
  /* 70px of headroom was left once the carousel came down, so the face
     takes it: 152 -> 210, the biggest that still clears the fold with the
     carousel, the control row and the AI button all above it. */
  /* 210 did NOT fit: it measured -60px past the fold, not the +12 the
     arithmetic predicted, so the face is not the only thing that grows with
     it. Back to the size actually measured as clearing the fold (+70px of
     headroom). Finding the true maximum needs one more measured pass, not
     another prediction. */
  /* NO VERTICAL CAP. 74px was shorter than the strip's own content, so it
     CLIPPED — Jon's half-visible template. A carousel scrolls sideways; it
     must never hide part of a thumb to save height. The thumbs are sized
     instead, which buys the same fold budget honestly. */
  .dz-tpl-row { max-height: none; }
  .dz-tpl-row img { height: 46px; width: auto; }
  .dz-tpl-row button { min-height: 44px; padding: 2px 4px; }
  .dz-tpl-row button span { font-size: 9px; line-height: 1.1; }
  .dz-car-arw { min-height: 44px; }
}

/* ── CONFIRM'S ORDER: TOTAL -> BUTTON -> MAP (Jon-ruled 2026-08-12) ──────
   Measured at 1366x768 before: the rail ran map (340) -> meta (48) ->
   pricing (348), which put Book my drop at top 909 against a 768 fold —
   about 196px below it, exactly as Jon reported. The money the customer is
   about to spend, and the button that spends it, were behind a map.
   The rail is a block container, so it becomes a flex column and the three
   slots are reordered by intent rather than by markup position — the map is
   context and goes last. #dk-solo-cta lives inside the pricing slot, so it
   travels with the total and lands directly under it, which is also what
   gives it its clearance from the name/email capture. */
/* ── THE MAP IS FIRST IN CONFIRM, AT EVERY WIDTH AND EVERY STATE ─────────
   Jon, 2026-08-12: "The map is the first block in CONFIRM at every width and
   every state. Never reordered, never relocated. Its height is capped so the
   total and Book my drop still clear the fold. The map gives up height; the
   order never changes."

   This SUPERSEDES the flex reorder that used to sit here, which sent the map
   to order:3 above 1200 so the total and the button could climb above it.
   That solved the fold by moving the furniture; the ruling solves it by
   taking the height out of the map instead. The reorder is removed at source
   rather than overridden — the map's position is now the markup's, which is
   the only arrangement that cannot silently differ between two widths.

   The cap itself cannot live here: CSS cannot know how tall the total, the
   button and the fields under them come out at a given width. It is measured
   at paint and applied to the slot, floored at one tap target so the expand
   control never becomes unreachable. See mapCap() in site.js. */

/* ── CONFIRM THE DESIGN: LOCKED MEANS VISIBLY LOCKED (Jon 2026-08-12) ────
   Tap-to-edit and the carousels go inert while the design is confirmed, and
   the way back is any deliberate touch inside DESIGN — which unlocks and
   withdraws the tick. Nothing here latches: the whole state is one attribute
   on <html>, and guidance re-derives from it every pass. */
.dz-tick {
  flex: 0 0 auto; min-height: 44px; min-width: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--pf-white); color: var(--pf-grey-600);
  border: 1px solid var(--pf-border); border-radius: var(--r-btn); cursor: pointer;
}
.dz-tick.is-ready { color: var(--pf-black); border-color: var(--pf-black); }
.dz-tick[aria-pressed="true"] { background: var(--pf-black); color: #fff; border-color: var(--pf-black); }
.dz-tick[aria-disabled="true"] { opacity: .55; }
.dz-tick:hover, .dz-tick:focus-visible { border-color: var(--pf-orange); color: var(--pf-orange); }
.dz-tick[aria-pressed="true"]:hover { color: #fff; }
html[data-pf-design-locked="1"] .dz-regions { pointer-events: none; opacity: .35; }
html[data-pf-design-locked="1"] .dz-tpl-row,
html[data-pf-design-locked="1"] .dz-car-arw { pointer-events: none; opacity: .5; }
html[data-pf-design-locked="1"] .dz-face-prev { outline: 2px solid var(--pf-black); outline-offset: 3px; }

/* ── THE LAST SLACK IN THE PHONE'S DESIGN WINDOW (2026-08-12) ────────────
   Measured stack at 390 with DESIGN live: 400px sits ABOVE the zone (header,
   nav band, the two collapsed section tabs) and the zone itself is 447 —
   header 41, a 4px empty form, gallery 240, controls 96, AI 48. The chrome
   inside the window is already near minimal, so the flyer is capped by page
   furniture above it, not by the design window. What is left inside is taken
   here and given to the face: the empty form's box, and the section header's
   own margin. Never from the controls — they hold the 44px floor. */
@media (max-width: 979px) {
  .zone[data-zone="design"] > #pf-designer-form:empty { display: none; }
  .zone[data-zone="design"] > .zone-lab { margin-bottom: 2px; }
}

/* ── JON'S MOBILE WALK, items 5, 7 and the control row ───────────────────
   5. THE AI BUTTON SAT FLUSH AGAINST THE CONTAINER EDGE. It needs its own
      clearance below, and so does Generate inside the options — a button
      touching the edge reads as clipped rather than placed. */
.dz-ai { margin-bottom: 12px; }
.dz-ai-opts { padding-bottom: 12px; }
.dz-ai-opts .dz-gen { margin-bottom: 10px; }

/* 7. THE TICK GLISTENS UNTIL IT IS LOCKED, so it reads as an action rather
      than decoration — and SETTLES once the design is confirmed, because a
      thing that keeps shimmering after you have pressed it is noise. Shares
      the AI button's sheen so the two speak the same language, and drops it
      under prefers-reduced-motion exactly as that one does. */
.dz-tick.is-ready:not([aria-pressed="true"]) { position: relative; overflow: hidden; }
.dz-tick.is-ready:not([aria-pressed="true"])::after {
  content: ""; position: absolute; inset: 0 auto 0 -60%; width: 60%;
  background: linear-gradient(100deg, transparent, rgba(253, 54, 1, .30), transparent);
  transform: skewX(-18deg); animation: dz-sheen 4.5s ease-in-out infinite;
}
.dz-tick[aria-pressed="true"]::after { content: none; } /* confirmed: it settles */
@media (prefers-reduced-motion: reduce) { .dz-tick.is-ready::after { animation: none; } }

/* 3. ONE RESPONSIVE LINE, and the tick gets its own line on a phone rather
      than being squeezed onto the end of one. Every control keeps the 44px
      floor on ITSELF, never on a wrapper — that distinction has cost two
      fixes already. */
.dz-primary .dz-under-block > .dz-tick { min-height: 44px; min-width: 44px; }
/* ITEM 2: the tick does NOT need its own row — it joins the line above.
   It glistens well; a whole row for one control is space the flyer wants. */
@media (max-width: 979px) {
  .dz-primary .dz-under-block > .dz-tick { flex: 0 0 auto; }
}

/* ── MOBILE WALK item 4: the template strip centres, even with one in it ──
   A single-template strip sat left-aligned against the arrows, which reads as
   a row that failed to fill rather than one deliberate item. Centred
   responsively: the strip centres its content when it does not overflow, and
   falls back to normal scrolling the moment it does, so centring never fights
   the scroll the carousels need. */
.dz-tpl-row, .pfsh-tpl-row, #dz-cell-looks {
  justify-content: safe center;
}
/* NOT margin-inline:auto — that collapsed the strip to its content. Measured
   on Shared at 390: the row came out 60px wide and clipping vertically, which
   is the "cut off, half visible" template. A flex row centres its CONTENT
   with justify-content; auto margins centre the BOX, and shrink it. */
.dz-tpl-row, #dz-cell-looks { width: 100%; min-width: 0; }

/* ── THE FACE IS FLOORED BY THE MAPS AND MAXIMISED BY THE FOLD ───────────
   Jon, 2026-08-12: "Hard floor, derived not chosen... then maximise. Above
   that floor, the face takes every pixel available. The carousel pays first."

   The floor arrives from the region maps in the DERIVED block at the top of
   this file — regions x tap floor — so it is not a number anyone typed, and
   the eight measured heights this rule replaces (250, 190, 168, 158, 152,
   150, 148, 130) are gone at source rather than overridden. Each of them was
   a screenshot's worth of truth, and each needed re-measuring whenever
   anything above or below the face moved.

   THE WIDTH IS THE DIMENSION SET, NOT THE HEIGHT. Setting the height and
   letting max-width clamp the width is what stretched the flyer to 787x414
   — an explicit height beats aspect-ratio, so the ratio silently loses. With
   the width set and the height derived from it, the box cannot deform: it
   either fits its column or the column caps it, and the proportions hold
   either way.

   The height budget is the fold minus everything that shares it, and that
   subtraction uses the very token the carousel sizes itself with, so the two
   can never drift apart the way a hand-summed budget does. */
.dz-face-block[data-face="front"] .dz-face-prev,
.dz-face-block[data-face="back"] .dz-face-prev {
  --pf-face-h: max(var(--pf-face-floor-h),
                   calc(var(--pf-face-budget, 100svh) - var(--pf-face-chrome)));
  /* THE FLOOR WIDTH IS DERIVED FROM THE RATIO THE BOX ACTUALLY USES (measured,
     2026-08-25). `--pf-face-floor-w` is defined in `:root`, where the ratio
     tokens are still A4's PAPER ratio 210/297 — but this box's `aspect-ratio`
     is the ARTWORK's own, set per look on the element (1632/2290 on the
     specimen). Two ratios, one floor: the min-width held 186.67 and the aspect
     turned it into **261.9px of height against a 264px floor**, so every width
     narrow enough to sit on the floor sat 2px under it — 1200, 1260 and 1320
     all failed the gate for it. Redeclared here, where the element's own ratio
     is in scope, it is the same number the box is drawn with. */
  --pf-face-floor-w: calc(var(--pf-face-floor-h) * var(--pf-face-ratio-w) / var(--pf-face-ratio-h));
  width: min(100%, calc(var(--pf-face-h) * var(--pf-face-ratio-w) / var(--pf-face-ratio-h)));
  min-width: var(--pf-face-floor-w);
  height: auto;
  aspect-ratio: var(--pf-face-ratio-w) / var(--pf-face-ratio-h);
  margin-inline: auto;
}
/* side by side, each face gets half the room, so the budget halves with it */
@media (min-width: 980px) {
  .dz-face-block[data-face="front"] .dz-face-prev,
  .dz-face-block[data-face="back"] .dz-face-prev {
    --pf-face-h: max(var(--pf-face-floor-h),
                     calc(var(--pf-face-budget, 100svh) - var(--pf-face-chrome)));
  }
}

/* THE CAROUSEL PAYS FIRST, AND PAYS DOWN TO A THUMBNAIL THAT IS STILL A
   THUMBNAIL. Its floor is derived too: a thumb tappable across its short
   edge, which for an A4 thumb sets the height. Below that it stops being a
   picture of a flyer and becomes a smudge, so that is where it stops paying
   and the fold has to come from somewhere else. The 148px/164px widths it
   replaces were chosen against one screen. */
.dz-looks img { width: auto; height: var(--pf-thumb-h); }

/* ── THE SHEET THEIR CARD LANDS IN (Jon-ruled 2026-08-14) ────────────────
   A sample of the sheet, at a glance, with their space in place — so a
   customer buying a cell can see what they are buying. The sheet's own
   proportions (A4 portrait, a 4x8 lattice) so it reads as the artefact and
   not as a diagram; the other cells are house tint, never invented ads. */
.dz-sheetmock[hidden] { display: none; }
.dz-sheetmock {
  /* THE WHOLE SHEET, NOT A CROP OF IT (Jon-ruled 2026-08-14). At 190px the
     32 spaces read as a zoomed fragment rather than the artefact, which is
     the opposite of what a sample is for: the customer is buying a place in
     a publication and needs to see the publication. */
  margin: 10px auto 0; width: min(100%, 260px);
  display: flex; flex-direction: column; gap: 6px;
}
/* ONE FACE, FOUR BLOCKS, SIXTEEN SPACES (Jon-ruled 2026-08-16). The grid is
   the four BLOCKS; each block holds its four cells. The boundary between
   blocks is drawn, because the shapes are defined against it — a double is a
   block's half and a quad is a whole block, and without the boundary neither
   has anything to be half or whole OF. */
/* ── THE SAMPLE IS SMALLER WHILE IT IS ONLY A SAMPLE (Jon-ruled 2026-08-24:
   "280px in the nothing-chosen state, full height once a space is held") ────
   Measured before ruling, squeezed in the live page: a 280px grid puts the
   cells at 55x59 and the blocks at 121x129 — still plainly cells, and the
   block boundaries a double and a quad are DEFINED against are still visible,
   which is what stops the sample explaining nothing. Below about 200 they
   become strips.
   `data-mine` is written by the painter from the hold, so the picture, its
   caption and its height all answer to one reading. Full height returns with
   the space. */
#dz-sheetmock[data-mine="0"] .dz-sheetmock-grid {
  height: 280px; min-height: 0; aspect-ratio: auto;
}
.dz-sheetmock-grid {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 7px;
  aspect-ratio: 210 / 297; padding: 7px 5px;
  background: var(--pf-white); border: 1px solid var(--pf-border);
}
.pfsh-sumline:empty, .pfsh-sumprice:empty { display: none; }
.dz-sheetmock-block {
  display: grid; grid-template-columns: repeat(2, 1fr); grid-auto-rows: 1fr; gap: 2px;
  padding: 3px; border: 1px solid var(--pf-border); border-radius: 2px;
  background: var(--pf-white);
}
.dz-sheetmock-cell { background: var(--pf-grey-50); border-radius: 1px; min-height: 0; }
/* AND NO SLOT IS DRESSED AS CHOSEN (Jon-ruled 2026-08-14). The orange outline
   read as "this is your position", and slot choice is deferred — the engine
   places the cell. Implying a position we do not sell is a claim, so the
   space carries the customer's artwork and nothing that says WHERE. */
.dz-sheetmock-cell--mine {
  background: var(--pf-white); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  box-shadow: inset 0 0 0 1.5px var(--pf-black);
}
/* THE BOOKING REFERENCE, on the space it identifies. It is the engine's own
   cell key and it only appears once a placement exists — see the painter. */
.dz-sheetmock-ref {
  font: 700 8px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: .02em; color: var(--pf-black); padding: 1px 2px;
}
.dz-sheetmock-cap {
  font-size: 11px; letter-spacing: .01em; color: var(--pf-grey-600); text-align: center;
}

/* ── SHARED'S CELL TAKES THE SAME BUDGET, IN ITS OWN PROPORTIONS ─────────
   Measured at 960: the cell block was 1287px tall inside a 900px fold, which
   is why Shared failed the face-size gate at every width while solo passed.
   The cell is NOT A4 and has no region map, so it gets neither the derived
   floor nor the 210:297 ratio — a solo sizing governing another product's
   artefact is exactly what put a 252px cell in a 250px host once.
   What it does get is the same measured space, as a CAP rather than a set
   height: the host states a budget and the painter fits the artefact into it,
   which is the mechanism the sheet renderer already honours. */
.dz-face-block[data-face="cell"] .dz-face-prev {
  max-height: var(--pf-face-space, none);
  overflow: hidden;
}

/* ── THE FLIP ARROWS (Jon-ruled, repeated 2026-08-12) ────────────────────
   Understated, one each side of the flyer, and the Front/Back pair leaves
   the control row — a control's worth of space back. They only exist on a
   two-sided product and only where one face shows at a time; with both faces
   side by side there is nothing to flip to. */
.dz-gallery { position: relative; }
.dz-flip {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 3;
  width: 44px; height: 44px; display: none;
  align-items: center; justify-content: center;
  background: rgba(255, 255, 255, .82); color: var(--pf-black);
  border: 1px solid var(--pf-border); border-radius: 50%; cursor: pointer;
}
.dz-flip--prev { left: 2px; }
.dz-flip--next { right: 2px; }
.dz-flip:hover, .dz-flip:focus-visible { border-color: var(--pf-orange); color: var(--pf-orange); background: #fff; }
/* ARROWS ON BOTH VIEWPORTS (Jon, 2026-08-13): "Arrows either side of the
   flyer on both viewports, flipping front and back, replacing the Front/Back
   control." The 979px gate here made them a phone affordance, so on desktop
   the pair they replace was gone and nothing took its place — the removal had
   landed and the replacement never had.
   They sit OUTSIDE the face, hard against the gallery's edges, because an
   arrow over the artwork sits on a tap target and eats the edit it covers.
   The single-face rules stay as they are: when both faces show side by side
   the arrows have nothing to flip to, which is a different question from
   which viewport this is. */
.dz-gallery .dz-flip { display: inline-flex; }
.dz-flip--prev { left: max(2px, calc(50% - (var(--pf-face-floor-w) / 2) - 46px)); }
.dz-flip--next { right: max(2px, calc(50% - (var(--pf-face-floor-w) / 2) - 46px)); }
/* WITH BOTH FACES ON SCREEN THERE IS NOTHING TO FLIP TO, and the gate proved
   the cost of pretending otherwise: at 1440 the arrow landed on `contact` and
   would have eaten every tap on it. An arrow that flips nothing and swallows
   an edit is worse than no arrow, so where both faces show the pair stands
   down — which is not a viewport rule, it is a "what is on screen" rule, and
   at every width where one face shows the arrows are there. */
.dz-gallery[data-face-mode="both"] .dz-flip { display: none; }
/* SHARED HAS NO FRONT AND BACK (Jon-ruled 2026-08-14). It is ONE SIDE. A flip
   control on a product with a single face is Solo's vocabulary leaking again
   — the same class as the front/back ternary a cell could never answer — and
   an arrow that flips to nothing is a dead control sitting on the artwork.
   The zoom panel's own arrow stands down with it. */
[data-deck="card"] .dz-flip,
[data-product="card"] .dz-flip { display: none !important; }
/* the row lost the pair the arrows replace — REMOVED at source, so there is
   no rule here to hide it and nothing to restore it by accident */

/* ── THE FACE LABELS GO WHERE THE LAYOUT ALREADY SPEAKS (Jon 2026-08-12) ──
   Side by side on desktop, which face is which is self-evident, so "Front"
   and "Back" are labelling what the layout already says. They come off there.
   On a phone one face shows at a time, and the flip arrows say only "there is
   another side", not WHICH side you are looking at — so the label stays as
   the only thing that answers that. Hidden from sight, never from a screen
   reader: the carousels still need their accessible names, and an icon-only
   flip control is allowed to be silent to the eye, never to them. */
/* ── THE FACE SAYS WHICH FACE IT IS — FROM INSIDE THE PICTURE (Jon-ruled
   2026-08-24). The 2026-08-13 ruling put small text under the flyer; the
   artefact carries its own FRONT / BACK strip at its foot, so that was the
   same word twice, and the caption is DELETED on both viewports. The element
   and every rule that dressed it are gone rather than hidden; the picker keeps
   the word as its own accessible name. */

/* ── ONE FACE WHEN TWO WILL NOT FIT, AT ANY WIDTH ────────────────────────
   auto-fit stopped the back face escaping its section, and immediately
   exposed the state underneath: with one column the two faces STACK, two
   derived floors deep, and the control row went 331px past the fold from
   1200 to 1360. Neither gate alone sees this — containment is happy with a
   tall column and face-size is happy with two floored faces.

   The three-zone desktop gives DESIGN a ~325px column at those widths, so
   the viewport is the wrong thing to ask: 1200px of screen can hold two
   faces on /solo and cannot inside the deck. That is what container queries
   are for. Below two floors plus their gap, the gallery does what the phone
   already does — one face, flip between them — which is the arrangement
   Jon ruled for exactly this situation rather than a new one invented for
   the middle widths. */
.zone[data-zone="design"] { container-type: inline-size; }
/* ── AND THE FACE THAT IS NOT SHOWING IS NOT DRAWN (Jon-ruled 2026-08-24) ──
   This was a container query — one face below 623px of column, two above it —
   and the ruling retires the pair at every width: the flyer is bigger the way
   the handset has always shown it, and the flip is the way to the other side.
   The arrows therefore stand at every width too; they were the handset's
   control and they are the deck's now. */
.dz-gallery[data-face-shown="front"] .dz-face-block[data-face="back"],
.dz-gallery[data-face-shown="back"] .dz-face-block[data-face="front"] { display: none; }
.dz-gallery .dz-flip { display: inline-flex; }
}

/* ── A REFUSAL THAT SPEAKS WHERE THE FINGER IS ───────────────────────────
   Jon's law is that refusals speak on screen; his orientation ruling adds
   WHERE — at the control, not in a note elsewhere on the page. A message
   the customer has to go looking for is the same as no message, and the
   control stays live throughout: a greyed button explains nothing and
   leaves them guessing whether the design or the site is at fault. */
.dz-say {
  position: absolute; z-index: 60; max-width: min(280px, calc(100vw - 16px));
  margin: 0; padding: 8px 11px;
  background: var(--pf-black, #1A1A1A); color: #fff;
  font-size: 13px; line-height: 1.35; border-radius: 8px;
  box-shadow: 0 6px 18px rgba(0,0,0,.22);
}

/* ── THE CAPTURE ARRIVES WHEN IT IS ASKED FOR (Jon, 2026-08-13) ──────────
   Held until the primary CTA is pressed. It is what pays for the fold at 390
   and 1024 — roughly 90px spent before the customer had said they wanted to
   book anything — and it is better manners besides: show the price and the
   button, ask for details once they reach for it.
   Held, not absent: the markup ships with the fields in place, so the reveal
   is a state change on one attribute rather than a build step, and nothing
   has to be constructed at the moment the customer is trying to pay. */
#dk-capture[data-pf-capture="held"] { display: none; }

/* ── DESIGN, MOBILE: THE ORDER AND THE CENTRED CONTROL ROW ───────────────
   Jon's handset ruling, 2026-08-13. Below the flyer: templates carousel,
   then the control row — portrait, landscape, Reset, confirm tick — then
   Upload my own flyer, then the AI designer.

   The block was `display: grid` with default start alignment, which is why
   the row read left-aligned on a handset with the controls strung along one
   edge. It is a centred wrapping row now, at every width rather than at a
   breakpoint, because the ruling says "at every width in the sweep" and a
   breakpoint is how the gaps between gated widths get their defects.

   UPLOAD MOVES BY ORDER, not by markup surgery. Its button, its note and its
   file input are one unit in the source and splitting them risks stranding
   the note away from the control it explains — the fault the capture ruling
   named for the CONFIRM capture. Order applies identically at every width, so
   it cannot make two widths disagree, which was the objection to the CONFIRM
   reorder this file removed. */
.dz-under-block[data-under="solo"], .dz-under-block[data-under="card"] {
  display: flex; flex-wrap: wrap; gap: 8px;
  align-items: center; justify-content: center;
}
/* ORDER CORRECTED (Jon, 2026-08-13, supersedes the earlier mobile order):
   templates carousel, Upload my own flyer, control row, AI designer. Upload
   comes BEFORE the row now — the earlier ruling put it after, and this is the
   one that stands. */
.dz-under-block [id$="-art"], .dz-under-block .upload-note,
.dz-under-block [id$="-upnote"], .dz-under-block input[type="file"] { order: 1; }
.dz-under-block .seg--orient, .dz-under-block .dz-reset, .dz-under-block .dz-tick { order: 2; }
/* the upload unit takes its own line under the row it follows */
.dz-under-block [id$="-art"] { flex: 1 1 100%; }
.dz-under-block .upload-note, .dz-under-block [id$="-upnote"] { flex: 1 1 100%; text-align: center; }
/* every control on the row carries the finger floor, and none of them shrink
   below it — a control row that fits by getting smaller than a thumb has not
   fitted */
.dz-under-block .seg--orient, .dz-under-block .dz-reset, .dz-under-block .dz-tick {
  min-height: 44px; flex: 0 0 auto;
}
.dz-under-block .seg--orient button { min-height: 44px; min-width: 44px; }

/* ── ONE LINE PER OPTION BUTTON (Jon's handset ruling, 2026-08-13) ───────
   "The option buttons wrap to multiple lines. Streamline them to a single
   line each at every swept width."
   The complaint is each BUTTON's text breaking mid-label, not the group
   taking two rows — so the labels are held on one line and the GROUP is
   allowed to wrap instead. That way nothing is shortened: "Visit us in
   store" and "Show this flyer" survive intact, and no customer-facing
   wording had to be invented or cut to make a layout fit. */
.dz-ai-opts .seg { flex-wrap: wrap; }
.dz-ai-opts .seg button { white-space: nowrap; flex: 0 0 auto; min-height: 44px; }

/* ── THE ZOOM CONTROLS (Jon, 2026-08-13) ─────────────────────────────────
   Steps, not a slider. A slider on a phone is a drag, and drag belongs to
   panning — the ruling is that panning takes priority and anything that
   fights it is dropped. The flip arrow sits here too so the gesture is never
   the only route to the other face. */
/* THE CONTROLS GET THEIR OWN BAND, NOT THE FLYER'S (2026-08-14). They were
   absolutely positioned over the bottom of the panel, which is empty space
   only while the artwork is letterboxed. The moment the picture fills the
   panel — every level from the derived floor upwards, and at rest on Shared
   from 768 up — the cluster sat ON the contact band and took its centre:
   elementFromPoint at the band's middle answered #dz-zoom-ctl, so a finger
   aiming at the contact details pressed "zoom in" instead. Measured, not
   theorised: 1440 solo @1.17x and 1440 card @1.01x both lose `contact` to it.
   Reserving a row costs a few pixels of face and cannot overlap by
   construction — the same reasoning as the tap targets that became their own
   bands rather than growing over their neighbours. */
.dz-zoom { flex-direction: column; }
/* AND THE FACE IS A WINDOW, NOT A SPILL. Nothing clipped the zoomed artwork,
   so at 2.4x and up the picture painted straight out of the panel and over
   the whole page — over the control row it had just been given, and under the
   guide bar at the top of the screen. The gate reads it exactly as the
   customer meets it: `contact under button` is the zoom stepper wearing the
   flyer, `headline under div#pf-guide` is the guide bar wearing it. One
   cause, two symptoms, and panning only means something inside a window. */
.dz-zoom-face { flex: 1 1 auto; height: auto; min-height: 0; overflow: hidden; position: relative; }
.dz-zoom-ctl {
  position: static; transform: none; flex: none; margin-top: 10px;
  display: flex; gap: 8px; z-index: 4;
}
.dz-zoom-ctl button {
  min-width: 44px; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.92); color: var(--pf-black, #1A1A1A);
  border: 1px solid var(--pf-border); border-radius: 50%;
  font-size: 20px; line-height: 1; cursor: pointer;
}
.dz-zoom-ctl button:hover, .dz-zoom-ctl button:focus-visible {
  border-color: var(--pf-orange); color: var(--pf-orange); background: #fff;
}
.dz-flip--zoom { position: static; transform: none; width: 44px; height: 44px; }
/* while zoomed, the artwork is dragged — the browser must not pan the page
   underneath it, and the picture must not be picked up as an image */
.dz-zoom-face[data-pf-zoom]:not([data-pf-zoom="1"]) { touch-action: none; cursor: grab; }
.dz-zoom-face img { -webkit-user-drag: none; user-select: none; }

/* THE ZOOM IS INHERITED, NOT ASSIGNED (2026-08-14). The zoom painter replaces
   the image, so an inline transform on it is discarded on the next repaint
   while the region layer keeps the scaled numbers it just derived — measured
   as a layer of 550x772 sitting over an unscaled 470x660 picture. The level
   lives on the FACE, which is not replaced, and every image painted into it
   inherits the transform without anything having to re-apply it. */
.dz-zoom-face img, .dz-zoom-face canvas {
  transform-origin: 50% 50%;
  transform: translate(var(--pf-zoom-x, 0px), var(--pf-zoom-y, 0px)) scale(var(--pf-zoom, 1));
}

/* THE SUB-FLOOR OFFER (Jon-ruled 2026-08-14). Grown targets are gone, so a
   thin band is genuinely thin, and a tap that lands on one and does nothing
   useful is a silent refusal. It speaks where the finger is and offers the
   thing that fixes it — the zoom level at which THAT band is editable. */
/* .dz-say-go went with the "too small to edit" notice (Jon-ruled 2026-08-23) */

/* ── W1: THE PERSON PICKER (Jon's walk 2026-08-22) ─────────────────────── */
.pf-people{position:absolute;left:50%;top:8px;transform:translateX(-50%);width:min(92vw,360px);z-index:40;background:#fff;border:1px solid #e6e2dd;border-radius:10px;box-shadow:0 12px 32px rgba(0,0,0,.18);padding:12px 12px 10px;text-align:left}
.pf-people-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;font-size:15px}
.pf-people-x{border:0;background:transparent;font-size:18px;line-height:1;width:36px;height:36px;border-radius:8px;cursor:pointer}
.pf-people-x:hover,.pf-people-x:focus-visible{background:#f3f0ec;outline:2px solid #FD3601;outline-offset:-2px}
.pf-people-lab{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#6b6560;margin:6px 0 4px}
.pf-people-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(56px,1fr));gap:6px;max-height:38vh;overflow:auto;padding:2px}
.pf-people-grid button{position:relative;aspect-ratio:2/3;border:2px solid #e6e2dd;border-radius:8px;background:#f4f1ed;padding:0;overflow:hidden;cursor:pointer}
.pf-people-grid button img{display:block;width:100%;height:100%;object-fit:contain;object-position:bottom}
.pf-people-grid button[aria-pressed="true"]{border-color:#FD3601;box-shadow:0 0 0 2px rgba(253,54,1,.25)}
.pf-people-grid button:focus-visible{outline:2px solid #FD3601;outline-offset:2px}
.pf-people-status{font-size:13px;color:#6b6560;margin:6px 0 0;min-height:1em}
.pf-people-status:empty{display:none}
.pf-people-doors{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
.pf-people-doors .btn{flex:1;min-height:44px}

/* ── W5: RESET ASKS FIRST ─────────────────────────────────────────────── */
/* ── THE SWITCH DIALOG WEARS THE ASK'S OWN CLOTHES (measured, 2026-08-25) ──
   `#pf-switch-ask` was built with the reset dialog's SHAPE and none of its
   STYLE: no rule named it, so it rendered as a plain block at the very end of
   the document, and the focus courtesy that puts the cursor on the cautious
   button scrolled the customer to the bottom of the page to reach it. That is
   the jump Jon walked into - 143 -> 3601 at 1600 - and it was not the upload
   path at all.
   `gate-switch-words` was green about it because it asserted the words were
   PAINTED, which a block div satisfies. Presence is not paint; paint in the
   right place is not the same as paint. The gate now asks where it stands. */
#pf-reset-ask,#pf-switch-ask{position:fixed;inset:0;z-index:1000;background:rgba(20,22,26,.45);display:flex;align-items:center;justify-content:center;padding:24px}
.pf-ask-card{background:#fff;border-radius:12px;padding:20px 20px 16px;max-width:360px;width:100%;box-shadow:0 16px 40px rgba(0,0,0,.25)}
.pf-ask-q{font-size:20px;font-weight:800;letter-spacing:-.01em;margin:0 0 6px}
.pf-ask-sub{font-size:14px;color:#5b5753;margin:0 0 14px}
.pf-ask-row{display:flex;gap:10px}
.pf-ask-row .btn{flex:1;min-height:44px}

/* TO-SPEC A3 (2026-08-22): Solo books into the order. A success at the Book
   button is not an alert; the sticky bar answers a refused Pay in words. */
.dk-cta-note.is-ok { color: var(--pf-green); }
.pfc-sb-msg { margin: 6px 0 0; font-size: 13px; line-height: 1.35; color: var(--pf-attention, #FD3601); }
#dk-solo-cart:empty { display: none; }
#dk-solo-cart { margin-top: 12px; }

/* P3 (2026-08-23): the checkout host holds the frame's resting height from the
   moment Pay is pressed, so Stripe's arrival fills a space that already exists */
.pfc-checkout.is-mounting { min-height: 790px; }            /* measured: 786 on the phone */
@media (min-width: 1200px) { .pfc-checkout.is-mounting { min-height: 880px; } }   /* measured: 874 on desktop */

/* P1 (2026-08-23): the confirmation in CONFIRM, unmistakable; slots with no
   words yet paint nothing rather than a placeholder */
.zone[data-zone="confirm"] > .pfc-done { margin: 12px 16px 8px; }
.pfc-done-art { margin: 10px 0 6px; }
.pfc-done-art img { display: block; max-width: 180px; max-height: 240px; border: 1px solid var(--pf-border); border-radius: 6px; background: #fff; }
.pfc-done-mail:empty, .pfc-next:empty { display: none; }
.pfc-receipt dt small { display: block; font-weight: 400; color: var(--pf-grey-600, #6B6B6B); font-size: 12px; }
html.pf-paid #pfc-sticky, html.pf-paid .pfc-pay { display: none !important; }

/* the checkout's own row on desktop (2026-08-23): full width beneath the
   three columns, centred, the deck's card treatment */
.pfc-row { grid-column: 1 / -1; display: flex; justify-content: center; }
.pfc-row > .pfc-checkout { width: 100%; max-width: 640px; background: #fff; border: 1px solid var(--pf-border); border-radius: 12px; padding: 16px; }

/* THE MONEY MOMENT (Jon-ruled 2026-08-23): one centred dialog, the deck's
   card treatment; full-bleed on a handset because the payment frame is
   ~790px and a centred box would scroll inside itself. */
.pf-money { border: 0; padding: 0; background: transparent; max-width: none; max-height: none; }
.pf-money::backdrop { background: rgba(26, 26, 26, .55); }
/* CENTRED, AND SAID SO IN CSS RATHER THAN ASSUMED (Jon's walk, 2026-08-24:
   "the modal is not centred — it paints HARD LEFT"). A modal <dialog> is
   centred by the user agent with margin:auto, and this stylesheet's global
   reset — `* { margin: 0; padding: 0 }`, line 137, there since day one — takes
   that away. The dialog has therefore NEVER been centred; I reported it was,
   from a measurement of its BOX (640x749) that never asked where the box was.
   Nothing about centring may be inherited from a default this file overrides. */
.pf-money[open] {
  margin: auto;
  /* 100% not 100vw: the viewport unit COUNTS THE SCROLLBAR and the dialog is
     laid out inside the box that does not, so 100vw centred it 15px left of
     centre — measured, after margin:auto had already fixed the hard-left. */
  width: min(640px, calc(100% - 24px)); max-height: calc(100vh - 24px);
  overflow: auto; background: var(--pf-white); border-radius: 14px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .28); padding: 18px 16px 16px; position: relative;
}
.pf-money-x {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  min-width: 44px; min-height: 44px; display: flex; align-items: center; justify-content: center;
  background: transparent; border: 0; cursor: pointer; color: var(--pf-ink, #1A1A1A);
}
.pf-money-t[hidden] { display: none; }
.pf-money-body > .pfc-checkout { min-height: 640px; }
.pf-money-body > .pfc-done { margin: 0; }
@media (max-width: 767px) {
  .pf-money[open] { width: 100vw; max-width: 100vw; height: 100vh; max-height: 100vh; border-radius: 0; padding: 14px 12px 12px; }
}
/* the finished order behind it */
.pfc-done--behind .pfc-new { margin-top: 6px; }
.pfc-row { display: none; }   /* the desktop checkout row is retired by the modal */

/* THE ZOOM'S LABELLED DOOR (Jon-ruled 2026-08-23) — the designer's cluster,
   the same control in the same place on both viewports */
.dz-full {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 44px; padding: 8px 12px; border-radius: var(--r-btn);
  border: 1px solid var(--pf-border); background: var(--pf-white);
  font: inherit; font-size: 13px; font-weight: 700; color: var(--pf-ink, #1A1A1A);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.dz-full:hover { border-color: var(--pf-ink, #1A1A1A); }
.dz-full:focus-visible { outline: 3px solid var(--pf-orange-tint, rgba(253, 54, 1, .35)); }

/* the honest wait, and the longer-than-usual state (Jon-ruled 2026-08-23) —
   both empty until his words land, and an empty slot paints nothing */
.dz-wait:empty { display: none; }
.dz-wait { margin: 6px 0 0; font-size: 12.5px; color: var(--pf-grey-600, #6B6B6B); }
[data-state="longer"] { font-style: normal; }

/* the two fields that fill the wait (Jon-ruled 2026-08-23) */
.dz-waitfields[hidden] { display: none; }
.dz-waitfields {
  display: grid; gap: 8px; margin: 10px 0 0; padding: 12px;
  border: 1px solid var(--pf-border); border-radius: 10px; background: var(--pf-white);
}
.dz-waitfields-t:empty { display: none; }
.dz-waitfields-t { margin: 0; font-size: 13px; font-weight: 700; }
@media (min-width: 700px) { .dz-waitfields { grid-template-columns: 1fr 1fr; align-items: end; } .dz-waitfields-t { grid-column: 1 / -1; } }

/* ── EDITING ONE BAND (Jon-ruled 2026-08-23) ──────────────────────────────
   "The tapped band is HIGHLIGHTED and unmistakable; the rest of the flyer
    GHOSTS back so attention sits on the one thing being edited." And: "the
    orange guidance boxes change state when one is being edited, so 'every band
    is orange' stops meaning nothing."
   The scrim is the ACTIVE target's own outer shadow — one element, no second
   layer to keep in step with the artwork, and it cannot drift from the band it
   surrounds. The layer clips it, so it covers the flyer and nothing else. */
.dz-regions.is-editing { overflow: hidden; }
.dz-regions.is-editing button[data-region] {
  opacity: .3; background: rgba(253, 54, 1, .04); border-color: rgba(253, 54, 1, .18);
}
.dz-regions.is-editing button[data-region]::before { opacity: .25; }
/* ── AND IT SPEAKS THE DECK'S OWN LANGUAGE (Jon-ruled 2026-08-24: "the
   treatment a live SECTION gets is what an edited BAND should get") ────────
   The live section is a 1px orange border with a 2px orange ring outside it,
   its header FILLED orange, and everything around it receding. So the edited
   band takes exactly that: the same `--pf-orange`, the same 1+2 boundary
   doubled to read at band size, the words themselves lifted on the brand tint
   the deck already uses for a live control (`--pf-orange-tint`), and the rest
   of the flyer DARKER than it was — the scrim was a 62% WHITE wash, which
   lightened a light flyer into a haze instead of pushing it back.
   No second visual language: every value here is a token this chrome already
   owns, and the scrim is still the target's own outer shadow, so it cannot
   drift from the band it surrounds. */
.dz-regions.is-editing button[data-region].is-editing-on {
  opacity: 1; z-index: 6;
  background: var(--pf-orange-tint, rgba(253, 54, 1, .20));
  border: 3px solid var(--pf-orange, #FD3601);
  box-shadow: 0 0 0 2px var(--pf-orange, #FD3601),
              0 0 0 9999px rgba(26, 26, 26, .55);
}
.dz-regions.is-editing button[data-region].is-editing-on::before { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .dz-regions button[data-region] { transition: opacity .14s ease-out, box-shadow .14s ease-out; }
}
/* THE BOX SITS AT THE BAND. The id beats the two rules that lay the editor out
   below the artwork — on the face and inside the zoom alike — so one anchoring
   answers both surfaces. */
#dz-editor.is-anchored {
  position: absolute; transform: none; margin: 0; z-index: 8;
  width: min(320px, 92%);
}

/* ── THE LIVE STAGE (Jon-ruled 2026-08-23) ────────────────────────────────
   The frame's picture and its live surface occupy the SAME box — whichever is
   showing, the region layer measures the same rect and the tap targets land in
   the same places. The live surface is inert to the pointer until the deck
   turns editing on, so a stray click cannot land inside the document. */
.pff-live { display: block; width: 100%; height: 100%; border: 0; background: #fff; }
.pff-live[hidden] { display: none; }
.pff-face[data-pff-live-on] .pff-art { display: none; }

 /* PAY TAKES BOOK'S PLACE, ON THE PHONE TOO (Jon-ruled 2026-08-24).
    The booking control is an <a> wearing button styling, and any author rule
    that sets `display` beats the [hidden] attribute's UA rule — so at 390 it
    kept its 308x53 box while reporting hidden:true, and Pay appeared 105px
    BELOW it, which is the defect exactly as Jon described it. Measured, not
    assumed: hidden:true with a painted box is the whole tell. */
#dk-solo-cta[hidden], #dk-card-cta[hidden], .dk-card-cta[hidden] { display: none !important; }

/* ── THE ZOOM RING (Jon-ruled 2026-08-24) ─────────────────────────────────
   Drawn in the gutter between the flyer and its host — measured at 15/20/20/28
   on desktop and 14/18/18/27 on the handset, so it never reaches a band. The
   region layer sits inside it and keeps the whole interior. */
.dz-ring[hidden] { display: none; }
/* ── IT HAS TO READ AS A LOCK-ON (Jon-ruled 2026-08-24: "it exists and Jon
   does not see it as one. Make it unmistakable — thick, orange, framing the
   whole flyer, appearing on HOVER and taking the click ANYWHERE outside the
   artwork") ────────────────────────────────────────────────────────────────
   It was 3px of transparent border that turned orange on hover and thickened
   to 5px only when the pointer was on the border itself — 15-28px of gutter,
   which is why it never announced anything. Now: 6px of the brand orange with
   its own halo, the whole frame, the moment the pointer is anywhere on the
   face. The click was already taken anywhere outside the bands (the ring is
   inset:0 under the region layer, which passes its gutter through); what
   changes is that it now LOOKS like the thing it always was. */
.dz-ring {
  position: absolute; inset: 0; z-index: 3;
  background: transparent; border: 3px solid transparent; border-radius: 6px;
  padding: 0; cursor: zoom-in; -webkit-tap-highlight-color: transparent;
  transition: border-color .12s ease-out, border-width .12s ease-out,
              box-shadow .12s ease-out;
}
.dz-ring-tag {
  position: absolute; top: -1px; right: -1px; transform: translateY(-100%);
  background: var(--pf-orange, #FD3601); color: #fff;
  font: 700 10px/1 Inter, system-ui, sans-serif; letter-spacing: .06em;
  text-transform: uppercase; padding: 3px 6px; border-radius: 4px 4px 0 0;
  opacity: 0; transition: opacity .12s ease-out;
}
/* DESKTOP: it answers the hover of the stage, and thickens on itself */
@media (hover: hover) {
  .dz-face-prev:hover > .dz-ring {
    border-width: 6px; border-color: var(--pf-orange, #FD3601);
    box-shadow: 0 0 0 2px rgba(253, 54, 1, .30), inset 0 0 0 2px rgba(253, 54, 1, .30);
  }
  .dz-face-prev:hover > .dz-ring .dz-ring-tag { opacity: 1; }
  /* on the ring itself it firms up again, so the gutter says "this press" */
  .dz-ring:hover {
    border-width: 7px;
    box-shadow: 0 0 0 3px rgba(253, 54, 1, .45), inset 0 0 0 2px rgba(253, 54, 1, .45);
  }
}
/* THE HANDSET HAS NO HOVER, so the ring is the standing SIGN. It is not the
   door: "Full size" on the artwork row is, because this is 14-27px of gutter
   and the finger floor is 44. */
/* THE HANDSET IS THE SAME SIGN, STANDING (unchanged in kind, 2026-08-24): it
   is 14-27px of gutter against a 44px finger, so it is still NOT the door —
   "Full size" on the artwork row is. It takes the ring's new weight so the two
   viewports say the same thing, and keeps pointer-events off so a thumb aimed
   at a band never lands on the zoom instead. */
/* ── AND ON A HANDSET IT IS THE DOOR, NOT ONLY THE SIGN (Jon-ruled
   2026-08-27: "pressing Edit does nothing — the core of the product is
   unreachable on a phone") ──────────────────────────────────────────────────
   MEASURED at 390 with a real touch profile: `#dz-ring` is 308x432, visible,
   in the viewport, tagged **EDIT** — and computed `pointer-events: none`, so
   `elementFromPoint` at its own centre returned `img.pff-art` and a real tap
   registered ZERO clicks on it. The page showed an orange-outlined picture
   labelled EDIT that was inert by construction, while the other door lives in
   the controls rail, which is now a 25px closed tab.

   THE GUARD THAT TURNED IT OFF WAS PROTECTING SOMETHING THAT IS NOT THERE.
   Its reason was "so a thumb aimed at a band never lands on the zoom instead"
   — but the stage carries NO band targets on a handset (`walk-arrangement`
   asserts exactly that: "the stage carries no band targets — it is a
   preview", and `#dz-regions` measures 10 present, 0 visible), and the door's
   own handler already refuses a press that lands on one:
   `if (e.target.closest('#dz-regions')) return; // an edit, not a zoom`.
   The belt was redundant and it was the thing breaking the door.

   The SIGN styling is unchanged — heavier border, tag always shown. Only the
   press is restored. */
@media (hover: none) {
  .dz-ring {
    border-width: 4px; border-color: rgba(253, 54, 1, .70); pointer-events: auto;
    box-shadow: inset 0 0 0 1px rgba(253, 54, 1, .25);
  }
  .dz-ring .dz-ring-tag { opacity: 1; }
}


/* ── THE ROW SCROLLS RATHER THAN WRAPPING, WHERE IT WOULD WRAP (Jon-ruled
   2026-08-24) — AND IT IS LAST IN THE FILE ON PURPOSE ───────────────────────
   At 1200 the deck becomes three columns and the design column drops from
   612px to 251px. The same controls could no longer share a line, the row went
   90px -> 200px in one pixel of window, and the flyer paid all of it:
   487px -> 331px.

   THIS RULE HAS BEEN WRITTEN THREE TIMES AND DEPLOYED INERT TWICE. Asking the
   browser which rules actually match this row is what ended it, and the answer
   is that FOUR later declarations re-wrap it, all at the same specificity, so
   position in the file is the whole game:
     · a SECOND copy of `.dz-under-block[data-under="solo"|"card"]` with
       `flex-wrap: wrap`, further down than the first;
     · `.dz-primary .dz-under-block { flex-wrap: wrap }`;
     · `.dz-under-block [id$="-art"] { flex: 1 1 100% }`, which is what puts
       Upload on a line of its own;
     · `.dz-primary .dz-under-block > #dk-solo-art { flex: 1 1 148px }`.
   And `#dz-primary` itself computes `display: block`, so the flex-wrap I first
   wrote on the wrapper was never a rule at all — the note at
   "THE ROW IS THE UNDER-BLOCK, NOT ITS WRAPPER (2026-08-12)" says exactly that,
   four hundred lines above where I wrote it.

   So it lives at the end, where nothing can follow it, and it names every
   selector that would otherwise win. The row scrolls on its own axis the way
   the look picker beside it already does: a narrowing column costs the row its
   width and never the flyer its height. The controls keep their finger floor,
   and the scrollbar is hidden because it is furniture. */
@media (min-width: 1200px) {
  .dz-under-block[data-under="solo"],
  .dz-under-block[data-under="card"],
  .dz-primary .dz-under-block {
    flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;
    justify-content: flex-start; -webkit-overflow-scrolling: touch;
    scrollbar-width: none; -ms-overflow-style: none;
  }
  .dz-under-block::-webkit-scrollbar { display: none; height: 0; }
  .dz-under-block .seg,
  .dz-under-block [id$="-art"],
  .dz-primary .dz-under-block > #dk-solo-art,
  .dz-primary .dz-under-block > #dk-card-art,
  .dz-under-block .upload-note { flex: 0 0 auto; }
}

/* ── THE AI CONTROL IS PART OF THE ROW (Jon-ruled 2026-08-24) — AND IT IS
   WRITTEN HERE, AT THE END, FOR THE REASON THE ROW ABOVE IS ─────────────────
   `#dz-primary` was a block stack: the artwork row, then the AI designer on a
   line of its own. One line, one control. It is a flex row now, so the two
   share a line — Full size, Upload my own flyer, Reset and the tick as they
   are, then the star.
   `WIDTH: AUTO` IS THE WHOLE FIGHT. `.dz-primary .dz-under-block` carries
   `width: 100%` further up the file, so the row filled the line and the star
   wrapped beneath it however the flex was declared — the walk measured
   `#dz-primary` at 102px with a 46px row inside it and said so.
   ITS PANEL STILL OPENS FULL WIDTH: an open `.dz-ai-opts` makes its own item
   take the whole row, so the brief is exactly where it was. */
.dz-primary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.dz-primary > .dz-under-block { flex: 1 1 auto; min-width: 0; width: auto; }
.dz-primary > .dz-ai { flex: 0 0 auto; margin-top: 0; }
.dz-primary > .dz-ai:has(> .dz-ai-opts:not([hidden])) { flex: 1 1 100%; }

/* ── THE PICKER FOLDED (Jon-ruled 2026-08-11, applied here 2026-08-24) ──────
   One quiet line carrying the look they chose and the way back, in the deck's
   own idiom — the same shape the booking rail uses beside Smart Match. The row
   itself is what folds; the line is the control that brings it back, which is
   what gate-mobile-composition requires of any folded picker. */
.dz-tpl-folded { display: none; }
.dz-tpl[data-folded="1"] > .dz-tpl-folded {
  display: flex; align-items: baseline; gap: 8px; width: 100%;
  padding: 6px 8px; min-height: 32px;
  background: #fff; border: 1px solid var(--pf-border); border-radius: 6px;
  font: 500 12px/1.3 Inter, system-ui, sans-serif; text-align: left;
  color: var(--pf-black); cursor: pointer;
}
.dz-tpl[data-folded="1"] > .dz-tpl-folded:hover,
.dz-tpl[data-folded="1"] > .dz-tpl-folded:focus-visible {
  border-color: var(--pf-orange);
}
.dz-tpl-folded-n { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dz-tpl-folded-c { flex: none; color: var(--pf-attention, #C42B00); font-weight: 700; }
.dz-tpl[data-folded="1"] > .dz-car { display: none; }

/* THE PAIR IS RETIRED (Jon-ruled 2026-08-25, deliberately superseding
   2026-08-24's "Full size takes the width it was not using"). Two faces at
   598px each cannot hold a landscape sheet, which needs 995 of width at this
   height, and they cost the picture 18% of its linear size against one face in
   the same dialog. One face, front or back, with the flip in the dialog's own
   control cluster — where it has always been. The rules that made the dialog a
   two-column grid are GONE rather than disabled, per the superseded-elements
   law, and nothing creates `#dz-zoom-face2` any more. */

/* ══ THE DESIGN SECTION'S ARRANGEMENT (Jon-ruled 2026-08-25) ═══════════════
   "LEFT: the template/look selection, as a slider. MIDDLE: the flyer, one
   face, a PREVIEW, not an edit surface. RIGHT: the control list — Upload my
   own, Flip, Reset, AI designer, and the tick at the bottom."

   THE RAILS ARE HIS NUMBERS, and they were ruled from a measurement: the
   middle is 685 less the rails and their gaps, the flyer is width-bound in
   every arrangement, and 90/140 gives **431x610** against the 396x555 the
   stacked shape gave. Slimmer rails give more picture and less room for a
   caption and a label; wider ones eat the whole gain (110/160 measured
   391x553, smaller than what it replaced).
   On the handset it is 56 and the controls stay a row beneath — his
   arrangement for that width, measured at 302x427 against 293x411.
   It sits at the end of the stylesheet because three rules in this file's
   design section have already been written correct and inert further up. */
.dz-rail { min-width: 0; min-height: 0; overflow: hidden; }
.dz-rail--looks .dz-tpl, .dz-rail--looks .dz-car { max-height: 100%; min-height: 0; }
.dz-rail--controls { overflow: visible; }
.dz-rail--looks { display: block; }
.dz-rail--controls { display: block; }

/* ── THE THREE COLUMNS START WHERE THEY START PAYING (Jon-ruled 2026-08-25,
   moves this breakpoint from 1200) ────────────────────────────────────────
   "Between 1200 and 1320 the flyer sits on its 188x264 floor - the same size it
   was before any of today's work, at one of the commonest laptop widths. The
   rails do not pay until 1380, so three columns before that costs the customer
   everything and returns nothing."
   Measured on the deployed deck, the rails take 90 + 140 + 24 of gap out of a
   row that is only 285 wide at 1200: the MIDDLE TRACK was **31px** there, 71 at
   1240, 111 at 1280, 151 at 1320 - the picture was not fitting its column at
   all, it was standing on its 188px min-width and spilling out of a track
   narrower than itself. Same reasoning as moving the pair rule from 1440 to
   1560: a breakpoint that fires before it pays.

   IT IS NOT 1380, AND IT IS NOT A WIDTH ALONE. 1380 was my number and it was
   the wrong comparison: it read the railed sizes against EACH OTHER (188
   floored, then 211 at 1380) instead of against the shape they replace.
   Measured against the stack, on the deployed page, both ways round:

     THE STACK IS HEIGHT-BOUND. It spends the whole width and then stops at one
     screenful: 358x502 from 1440 up on a 900-tall window, 264x370 on a
     768-tall one, and not a pixel more however wide the screen gets.
     THE COLUMNS ARE WIDTH-BOUND. The middle track is the row less 90 + 140 +
     24, so the picture is viewport - 1169: 211 at 1380, 271 at 1440, 331 at
     1500, 391 at 1560, 431 at 1600.

   So the crossing MOVES WITH THE HEIGHT - about 1540 on a 900-tall window,
   about 1430 on a 768-tall one - and one width could only be right for one
   of them. The rule is both: the columns stand from 1560, or from 1440 when
   the window is short enough that the stack has already hit its ceiling. A
   1536x768 laptop is the case that makes it worth saying - the stack gives it
   264x370 and the columns give it 367x515. */
@media (min-width: 1560px), (min-width: 1440px) and (max-height: 860px) {
  .zone[data-zone="design"] .dz-stagegrid,
  .zone[data-zone="design"] > .zone-body > .dz-rail--looks { }
  .zone[data-zone="design"] > .zone-body,
  .zone[data-zone="design"] {
    /* the three rails, in Jon's order and at his widths */
  }
  .dz-rail--looks { width: 90px; }
  .dz-rail--controls { width: 140px; }
  .dz-designrow {
    /* ── THE RAILS FLOAT (Jon-ruled 2026-08-25) ────────────────────────────
       "LANDSCAPE NEEDS WIDTH, SO THE INTERFACE GETS OUT OF THE FLYER'S WAY."
       Measured, the two rails were taking 90 + 140 + 24 of gap = 254px of a
       685 row — 37% — and a landscape sheet in what was left measured 431x307
       against the 431x605 portrait gets. Out of the flow they cost only the
       resting strip they stand in: 44px each side, which is the finger floor
       and the widest the strips ever need to be.
       They are ABSOLUTE now, in the row's own padding, so the picture is
       centred in everything between them; expanded, they open OVER the picture
       rather than pushing it, because a control that moves the thing you are
       looking at is worse than one that covers a margin. */
    display: grid; grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto;
    gap: 8px; align-items: start; position: relative;
    padding-inline: 44px;
    /* THE ROW TAKES THE SECTION'S OWN SIDE PADDING (measured, 2026-08-25).
       Jon's 431x610 was arithmetic on the section's width, 685 — and the row
       only ever had 651, because the section keeps 17px each side. With the
       rails at 90 and 140 that put the flyer at 397x557, which is what the
       first walk measured: the height the rails freed and the width they took
       cancelled out exactly. The row is full-bleed now and the number is his
       number. */
    margin-inline: -17px;
  }
  /* the column rules that used to live here now sit outside every media block
     — see THE RAIL IS A COLUMN WHEREVER IT IS A RAIL, at the end of the file */
  /* the control list stands up, and the tick goes last */
  .dz-rail--controls .dz-primary,
  .dz-rail--controls .dz-under-block[data-under="solo"],
  .dz-rail--controls .dz-under-block[data-under="card"] {
    display: flex; flex-direction: column; align-items: stretch;
    flex-wrap: nowrap; overflow: visible; gap: 8px; width: 100%;
  }
  .dz-rail--controls .dz-primary > *,
  .dz-rail--controls .dz-under-block > *,
  .dz-rail--controls .seg,
  .dz-rail--controls .dz-full,
  .dz-rail--controls .dz-reset,
  .dz-rail--controls .dz-ai,
  .dz-rail--controls .dz-ai-btn { width: 100%; flex: 0 0 auto; }
  .dz-rail--controls .dz-tick { order: 9; align-self: stretch; }
  .dz-rail--controls .dz-faces { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
}

/* ── THE HANDSET KEEPS THE STACKED SHAPE (Jon-ruled 2026-08-25) ────────────
   "293x411, slider below, unchanged. The stage is a preview now; editing is in
   the Edit view at 378x763, so the preview's width buys little, and floating
   the slider over the gutter would put it where the ring lives."
   Measured, that is the ruling's own arithmetic: a 56px rail leaves the picture
   263x369 against the 293x411 the stack gives, because 327 less the rail and
   its gap is 263 — the rail costs more on a phone than it returns. So the three
   parts stack in the order they always did: the picture, then its slider, then
   the controls. The row keeps the section's padding here (no full bleed), which
   is what holds the picture at 293. */
/* the complement of the columns query above, written out rather than inferred,
   so the two can never both be true or both be false at some width */
@media (max-width: 1439px), (max-width: 1559px) and (min-height: 861px) {
  /* THE AI DESIGNER SHARES THE CONTROL LINE HERE (Jon-ruled 2026-08-25: "a
     single collapsed line ... do not take 52px off the stack"). Stacked, it is
     one control among the others; the bar and its full-width panel belong to
     the three-column arrangement, where there is room under the stage for it
     to open into. */
  /* THE ORIENTATION REASON IS SPOKEN WHEN IT IS ASKED FOR, on the stacked
     shape. Standing open it measured 78px of a section that has none to spare
     and took the handset's picture from 293x411 to 245x344; a ghosted control
     that answers the press still tells the customer why, and
     `aria-describedby` means a screen reader never loses it. */
  .dz-rail--controls .dz-ai { width: auto; flex: 0 0 auto; }
  .dz-rail--controls .dz-ai-btn { width: auto; min-height: 44px; }
  .dz-designrow {
    display: flex; flex-direction: column; gap: 8px; margin-inline: 0;
  }
  .dz-designrow > #dz-gallery { order: 1; }
  .dz-designrow > .dz-rail--looks {
    order: 2; width: auto; max-height: none; overflow: visible;
  }
  .dz-designrow > .dz-rail--controls { order: 3; width: auto; }
  /* and the slider lies down again: the strip is the horizontal carousel it
     has always been below 1200, so none of the rail's column rules apply */
}

/* ── THE STAGE IS A PREVIEW: THE BAND BOXES COME OFF IT (Jon-ruled
   2026-08-25) — "but THE RING STAYS as the hover door to EDIT" ─────────────
   The small flyer is no longer an edit surface, so a layer of orange boxes on
   it would be a visible control that does nothing, which this build has ruled
   against twice. They come off the STAGE and stay in the big view, which is
   where editing happens now. The ring is untouched: it frames the picture on
   hover and takes the click anywhere around it, which is the stage's signal
   that the flyer is editable without claiming a band-level tap it no longer
   has. */
.dz-face-prev > .dz-regions { display: none; }
.dz-zoom .dz-regions { display: block; }

/* ── THE TWO DOORS ON A BLANK FLYER (Jon-ruled 2026-08-25) ─────────────────
   They stand ON the blank, centred, because the choice is about the thing they
   are standing on. Big enough to read as the way in and quiet enough not to be
   a poster: the deck's own control language, in the deck's own ink. */
.dz-doors[hidden] { display: none; }
.dz-doors {
  position: absolute; inset: 0; z-index: 5;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px; padding: 16px; pointer-events: none;
}
.dz-doors > .dz-door { pointer-events: auto; }
.dz-door {
  display: inline-flex; flex-direction: column; align-items: center; gap: 6px;
  min-width: 148px; min-height: 76px; padding: 12px 14px;
  background: var(--pf-white); color: var(--pf-black);
  border: 2px solid var(--pf-border); border-radius: 10px;
  font: 700 13px/1.25 Inter, system-ui, sans-serif; text-align: center;
  cursor: pointer; box-shadow: 0 6px 18px rgba(0, 0, 0, .10);
  transition: border-color .12s ease-out, box-shadow .12s ease-out;
}
.dz-door:hover, .dz-door:focus-visible {
  border-color: var(--pf-orange); box-shadow: 0 8px 22px rgba(253, 54, 1, .18);
}
.dz-door svg { color: var(--pf-orange); }
/* the doors stand side by side only where the picture is wide enough to hold
   them: 148 + 179 + 12 of gap is 339, and below the rail breakpoint the flyer
   is narrower than that, so they stack the way they do on the handset */
@media (min-width: 1560px), (min-width: 1440px) and (max-height: 860px) { .dz-doors { flex-direction: row; } }
/* the gallery is the doors' positioning context */
.dz-gallery { position: relative; }
/* an unset or upload section has no template rail — his ruling, and the JS
   hides it; this keeps the grid honest when it does */
.zone[data-zone="design"][data-dz-mode="upload"] .dz-rail--looks,
.zone[data-zone="design"][data-dz-mode="unset"] .dz-rail--looks { display: none; }

/* ── THE THREE COLUMNS ARE PLACED, NOT AUTO-FLOWED (measured, 2026-08-25) ───
   Hiding the look rail — which is exactly what UNSET and UPLOAD do — took its
   child out of the grid, and auto-placement slid the GALLERY into the 90px
   track behind it. The cold stage measured **90 wide** with the flyer at its
   187px floor, and the two doors, centred on a 90px box, stood beside the
   picture instead of on it: the walk read `they stand ON the flyer -> [false,
   true]`. Each part is placed in its own track now, so an empty rail leaves an
   empty track and the flyer is the same 431 in every mode — taking a door must
   not resize the thing the door is standing on. */
@media (min-width: 1560px), (min-width: 1440px) and (max-height: 860px) {
  .dz-designrow > #dz-gallery { grid-column: 1; grid-row: 1; }
  .dz-designrow > .dz-aibar { grid-column: 1; grid-row: 2; }
  /* the rails are out of the grid entirely — they have no track to be placed
     in, and the picture is the only thing the row is sizing */
  /* THE WALL TREATMENT IS UNCONDITIONAL (Jon-ruled 2026-08-27): "One control,
     one treatment, at every width. Two treatments for the same rail is what
     'looks like a mess' meant in the first place, and the shape rule says a
     notch in the wall is a notch in the wall at 1600 as much as at 390."

     It had been a 10px card with a shadow here and a seam in the wall at 390,
     and I had scoped the gate to match rather than the build. This is the
     handset treatment at desktop proportions: a cream body carrying the seam
     on its INNER edge, squared flush against the wall it belongs to, rounded
     only on the side that opens, and no shadow in either state — a shadow is
     what makes a thing read as laid on top. */
  .dz-designrow > .dz-rail--looks,
  .dz-designrow > .dz-rail--controls {
    position: absolute; top: 0; z-index: 6;
    width: 44px; overflow: hidden;
    border: 0; border-radius: 0;
    background: var(--pf-cream, #FBF7F4);
    box-shadow: none;
    transition: width .13s ease-out, background-color .13s ease-out,
                border-color .13s ease-out;
  }
  .dz-designrow > .dz-rail--looks {
    left: 0; border-right: 1px solid var(--pf-border); border-radius: 0 8px 8px 0;
  }
  .dz-designrow > .dz-rail--controls {
    right: 0; border-left: 1px solid var(--pf-border); border-radius: 8px 0 0 8px;
  }

  /* AT REST: an edge tab, understated — the rail's own name down the strip in
     the deck's label voice. Nothing is clipped and nothing shouts.

     BUT IT IS NOT A WHISPER (Jon's walk, item 8: "THE LEFT RAIL'S 'TEMPLATES'
     LABEL IS BLOCKED OFF on desktop"). Measured at 1600: it is not blocked and
     nothing covers it — it was `--pf-muted` at `opacity: .75`, which is
     **5.48:1 on cream before the fade and less after it**. That is the exact
     value Jon rejected on the handset on 2026-08-26, where the ruling was
     "PLAINLY READABLE, NOT A WHISPER" and the label went to the ink at 15.30:1.

     THE RULING WAS SCOPED BY VIEWPORT WHEN THE THING IT DESCRIBES IS A LABEL —
     the same fault named twenty lines below about the rail's shape, and the
     ratified clause: a rule's limits must be expressed where it is enforced.
     A label is legible or it is not; 390 has nothing to do with it. */
  .dz-designrow > .dz-rail::before {
    content: attr(data-rail-name);
    position: absolute; left: 50%; top: 12px;
    transform: translateX(-50%) rotate(180deg);
    writing-mode: vertical-rl; text-orientation: mixed;
    font: 700 11px/1 Inter, system-ui, sans-serif;
    letter-spacing: .10em; text-transform: uppercase;
    color: var(--pf-black, #1A1A1A); opacity: 1;
    pointer-events: none; transition: opacity .1s ease-out;
  }
  .dz-designrow > .dz-rail > * { opacity: 0; transition: opacity .13s ease-out; }

  /* REVEALED: hover, keyboard focus anywhere inside, or a tap that sets
     data-open. THREE DOORS TO ONE STATE — hover is a shortcut, never the only
     way, so a keyboard and a touchscreen reach exactly the same rail. */
  .dz-designrow > .dz-rail--looks:hover,
  .dz-designrow > .dz-rail--looks:focus-within,
  .dz-designrow > .dz-rail--looks[data-open="1"] { width: 96px; }
  /* ── OPEN TO WHAT THE WORDS ACTUALLY NEED (Jon's walk, 2026-08-25) ──────
     Every label was truncated at 140: "Po…", "La…", "Fr…", "Ba…", "Upload my
     o…", "Confirm this…". Measured from each label's own scrollWidth against
     the width it had:
       Upload my own flyer  130 of 101 · Confirm this design 125 of 93
       Landscape 70 of 30 · Portrait 48 of 30 · Front 33 of 30 · Back 31 of 30
       Reset 36 of 36 ok · Edit 24 of 24 ok
     A full-width control keeps 37px of chrome (border, padding, the 17px glyph
     and its gap), so the longest label needs 130 + 37 = 167. **176** carries it
     with room, and every other label with it.
     THE TWO-STATE CONTROLS STACK WHILE OPEN, which is what makes 176 enough:
     side by side each half would get ~85 and "Landscape" alone needs 107, so
     two-up would have wanted 220 — 44px more picture spent on a control that
     is only open while it is being read. */
  .dz-designrow > .dz-rail--controls:hover,
  .dz-designrow > .dz-rail--controls:focus-within,
  .dz-designrow > .dz-rail--controls[data-open="1"] { width: 176px; }
  .dz-designrow > .dz-rail--controls:hover .seg[data-two],
  .dz-designrow > .dz-rail--controls:focus-within .seg[data-two],
  .dz-designrow > .dz-rail--controls[data-open="1"] .seg[data-two] {
    display: flex; flex-direction: column; gap: 4px;
  }
  .dz-designrow > .dz-rail--controls:hover .seg[data-two] > button,
  .dz-designrow > .dz-rail--controls:focus-within .seg[data-two] > button,
  .dz-designrow > .dz-rail--controls[data-open="1"] .seg[data-two] > button {
    width: 100%; justify-content: flex-start; padding: 0 10px;
  }
  /* the labels are read, not ellipsised, once the rail has opened for them */
  .dz-designrow > .dz-rail--controls:hover .dz-lab,
  .dz-designrow > .dz-rail--controls:focus-within .dz-lab,
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-lab { overflow: visible; text-overflow: clip; }
  .dz-designrow > .dz-rail--controls:hover .dz-full,
  .dz-designrow > .dz-rail--controls:hover .dz-reset,
  .dz-designrow > .dz-rail--controls:hover .dz-tick,
  .dz-designrow > .dz-rail--controls:hover .dz-under-block > button,
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-full,
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-reset,
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-tick,
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-under-block > button { justify-content: flex-start; }
  .dz-designrow > .dz-rail:hover,
  .dz-designrow > .dz-rail:focus-within,
  .dz-designrow > .dz-rail[data-open="1"] {
    background: var(--pf-white);
    box-shadow: none;
    overflow-y: auto; overflow-x: hidden;
  }
  .dz-designrow > .dz-rail:hover::before,
  .dz-designrow > .dz-rail:focus-within::before,
  .dz-designrow > .dz-rail[data-open="1"]::before { opacity: 0; }
  .dz-designrow > .dz-rail:hover > *,
  .dz-designrow > .dz-rail:focus-within > *,
  .dz-designrow > .dz-rail[data-open="1"] > * { opacity: 1; }

  /* ── THE AI DESIGNER'S BAR (Jon-ruled 2026-08-25) ──────────────────────
     Full width under the stage, where its panel has room to open. */
  .dz-aibar { width: 100%; }
  .dz-aibar > .dz-ai { width: 100%; }
  .dz-aibar .dz-ai-btn { width: 100%; min-height: 44px; }
}

/* ── THE GHOSTED ORIENTATION CONTROL (Jon-ruled 2026-08-25) ────────────────
   Ghosted, not hidden: the customer can see that landscape exists and read why
   it is not available, which is the difference between a locked door and a
   wall. The reason is in Jon's ratified words and only shows while the lock
   is on. */
.dz-orient-wrap { width: 100%; }
/* ── UNAVAILABLE READS AS UNAVAILABLE (Jon's walk, 2026-08-25) ────────────
   "A ghosted control must not wear the selected treatment. It should read
    plainly as unavailable."
   So the ghost is a state of its own, not a dimmed version of a live control:
   muted ink on a muted edge, a muted glyph, no orange anywhere, and the
   not-allowed cursor a customer's hand meets before their eye does. The
   line-through goes with the pressed look — a struck-out label reads as
   something that WAS chosen, which is the opposite of the truth here. */
.dz-orient-wrap[data-locked="1"] button[data-v="landscape"],
.dz-rail--controls .dz-orient-wrap[data-locked="1"] button[data-v="landscape"] {
  color: var(--pf-muted, #6b6560);
  border-color: var(--pf-grey-200, #eee);
  background: var(--pf-grey-50, #fafafa);
  cursor: not-allowed;
  text-decoration: none;
}
.dz-orient-wrap[data-locked="1"] button[data-v="landscape"] .dz-g { opacity: .5; }
/* and it never takes the hover or the pressed treatment, whatever it is told */
.dz-rail--controls .dz-orient-wrap[data-locked="1"] button[data-v="landscape"]:hover,
.dz-rail--controls .dz-orient-wrap[data-locked="1"] button[data-v="landscape"]:focus-visible,
.dz-rail--controls .dz-orient-wrap[data-locked="1"] button[data-v="landscape"][aria-pressed="true"] {
  color: var(--pf-muted, #6b6560);
  border-color: var(--pf-grey-200, #eee);
  background: var(--pf-grey-50, #fafafa);
}
.dz-orient-why {
  margin: 4px 0 0; font: 400 11px/1.35 Inter, system-ui, sans-serif;
  color: var(--pf-muted, #6b6560);
}
.dz-orient-why[hidden] { display: none; }

/* ══════════ ONE CONTROL SYSTEM (Jon-ruled 2026-08-25) ══════════════════════
   "One height, one width, two type sizes, two weights, one radius, one ink,
    six glyphs."
   WHAT IT REPLACES, measured on the deployed deck before it: four heights
   (39/44/46/56), five font sizes (12/13/13.33/14/17 — one of them the browser's
   own default, because "Confirm this design" had no type set at all), four
   weights (400/600/700/800), one square control among five rounded, and,
   stacked, six controls at six different widths (417/149/69/64/44/104).

   IT SITS AT THE END OF THE STYLESHEET ON PURPOSE. Four rules in this file's
   design section have already been written correct and inert further up, so
   everything the system needs to beat is named here rather than hoped about:
   `.dz-under-block > *` and `.dz-primary > *` (both width:100%), `[id$="-art"]`
   (flex 1 1 100%), `.deck-rail .seg button`, and the rail column rules in the
   1560 media block.

   AND IT PAYS FOR ITSELF. The stacked control row measured 205px on a handset,
   which is what had the picture at 269x378 against Jon's ruled 293x411. Three
   even rows of 44 with 8px gaps is 156 — 49px back, against a 48px shortfall. */

:root {
  --pf-ctl-h: 44px;          /* the finger floor, and now the only height */
  --pf-ctl-r: 10px;          /* the only radius */
  --pf-ctl-t: 13px;          /* the label size */
  --pf-ctl-cap: 11px;        /* the caption/edge-tab size — the second and last */
  --pf-ctl-gap: 8px;
}

/* ── ONE HEIGHT, ONE RADIUS, ONE TYPE, ONE INK ─────────────────────────── */
.dz-rail--controls .seg > button,
.dz-rail--controls > button,
.dz-rail--controls .dz-primary > button,
.dz-rail--controls .dz-under-block > button,
.dz-rail--controls .dz-full,
.dz-rail--controls .dz-reset,
.dz-rail--controls .dz-tick,
.dz-rail--controls .dz-ai-btn,
.dz-aibar .dz-ai-btn {
  min-height: var(--pf-ctl-h); height: var(--pf-ctl-h);
  display: inline-flex; align-items: center; justify-content: center;
  gap: 6px; padding: 0 10px; margin: 0;
  border: 1px solid var(--pf-border); border-radius: var(--pf-ctl-r);
  background: var(--pf-white); color: var(--pf-black);
  font: 700 var(--pf-ctl-t)/1.15 Inter, system-ui, sans-serif;
  letter-spacing: 0; text-align: center; text-transform: none;
  cursor: pointer; box-shadow: none;
  transition: border-color .12s ease-out, background-color .12s ease-out, color .12s ease-out;
}
/* the one primary — the only 800 in the system, and the only filled control */
.dz-rail--controls .dz-ai-btn,
.dz-aibar .dz-ai-btn {
  font-weight: 800;
  background: var(--pf-black, #1A1A1A); color: var(--pf-white); border-color: var(--pf-black, #1A1A1A);
}
.dz-rail--controls .seg > button:hover,
.dz-rail--controls .dz-full:hover, .dz-rail--controls .dz-reset:hover,
.dz-rail--controls .dz-tick:hover, .dz-rail--controls > button:hover,
.dz-rail--controls .dz-under-block > button:hover,
.dz-rail--controls .seg > button:focus-visible,
.dz-rail--controls .dz-full:focus-visible, .dz-rail--controls .dz-reset:focus-visible,
.dz-rail--controls .dz-tick:focus-visible, .dz-rail--controls > button:focus-visible {
  border-color: var(--pf-orange); color: var(--pf-orange);
}
/* pressed is the deck's own pressed: orange ink on an orange edge */
.dz-rail--controls .seg > button[aria-pressed="true"] {
  border-color: var(--pf-orange); color: var(--pf-orange);
}
/* the tick keeps its own confirm token — the one colour the system does not own */
.dz-rail--controls .dz-tick[aria-pressed="true"] {
  border-color: var(--pf-green); color: var(--pf-green);
}
.dz-rail--controls .dz-g { flex: 0 0 auto; width: 17px; height: 17px; }
.dz-rail--controls .dz-lab {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: inherit;
}
/* the second type size, and the only other one: captions and the edge tab */
.dz-rail .dz-tpl-row > button > span,
.dz-orient-why {
  font: 400 var(--pf-ctl-cap)/1.35 Inter, system-ui, sans-serif;
}

/* ── ONE WIDTH, IN THE OPEN RAIL: the rail's own ───────────────────────── */
@media (min-width: 1560px), (min-width: 1440px) and (max-height: 860px) {
  .dz-rail--controls .seg,
  .dz-rail--controls .dz-primary,
  .dz-rail--controls .dz-under-block,
  .dz-rail--controls .dz-orient-wrap { width: 100%; }
  .dz-rail--controls .seg { display: flex; gap: 4px; }
  .dz-rail--controls .seg > button { flex: 1 1 0; padding: 0 6px; }
  .dz-rail--controls > button,
  .dz-rail--controls .dz-primary > button,
  .dz-rail--controls .dz-under-block > button,
  .dz-rail--controls .dz-full, .dz-rail--controls .dz-reset,
  .dz-rail--controls .dz-tick, .dz-rail--controls .dz-ai-btn { width: 100%; }
  /* AT REST THE RAIL IS ITS GLYPHS. The labels go, the pressed half of each
     two-state control stands for the pair, and 44px is enough for all of it —
     which is the whole point of a rail that costs 44px. */
  .dz-designrow > .dz-rail--controls:not(:hover):not(:focus-within):not([data-open="1"]) .dz-lab { display: none; }
  /* ONLY A TWO-STATE CONTROL STANDS DOWN ITS OTHER HALF. Written as
     `.seg > button[aria-pressed="false"]` it also hid UPLOAD, which is a
     single-button seg that is simply not pressed — measured `display: none`,
     `0x0`, a control gone from the rail entirely. The two-state segs say so on
     themselves. */
  .dz-designrow > .dz-rail--controls:not(:hover):not(:focus-within):not([data-open="1"]) .seg[data-two] > button[aria-pressed="false"] { display: none; }
  .dz-designrow > .dz-rail--controls:not(:hover):not(:focus-within):not([data-open="1"]) .dz-orient-why { display: none; }
  .dz-designrow > .dz-rail--controls:not(:hover):not(:focus-within):not([data-open="1"]) .seg > button { padding: 0; }
}

/* ── ONE WIDTH, STACKED: even tiles that wrap into even rows ───────────── */
@media (max-width: 1439px), (max-width: 1559px) and (min-height: 861px) {
  /* THE WRAPPERS STOP BEING BOXES so every control is a sibling in ONE
     wrapping row. Nested flex rows each wrapped on their own, which is how six
     controls came to sit at six widths across three uneven lines. */
  .dz-rail--controls .dz-primary,
  .dz-rail--controls .dz-under-block[data-under="solo"],
  .dz-rail--controls .dz-under-block[data-under="card"] { display: contents; }
  .dz-rail--controls {
    display: flex; flex-wrap: wrap; gap: var(--pf-ctl-gap);
    align-items: start; align-content: start;
  }
  /* a two-state control takes half a row; a single control takes a third */
  .dz-rail--controls > .seg,
  .dz-rail--controls .dz-orient-wrap { flex: 1 1 calc(50% - var(--pf-ctl-gap)); width: auto; }
  .dz-rail--controls .dz-orient-wrap > .seg { width: 100%; }
  .dz-rail--controls .seg { display: flex; gap: 4px; }
  .dz-rail--controls .seg > button { flex: 1 1 0; min-width: 0; }
  .dz-rail--controls > button,
  .dz-rail--controls .dz-full, .dz-rail--controls .dz-reset,
  .dz-rail--controls .dz-tick, .dz-rail--controls .dz-ai {
    flex: 1 1 calc(33.333% - var(--pf-ctl-gap)); width: auto; min-width: 0;
  }
  .dz-rail--controls .dz-ai-btn { width: 100%; }
  /* the AI panel takes the whole row when it opens, and only then */
  .dz-rail--controls .dz-ai:has(> [aria-expanded="true"]) { flex-basis: 100%; }
  /* notes and hints are captions, not controls — they never join the tiling */
  .dz-rail--controls .upload-note,
  .dz-rail--controls .dz-waitfields,
  .dz-rail--controls .dz-orient-why { flex: 1 1 100%; }
  .dz-rail--controls .upload-note:empty { display: none; }
}

/* ── THE SMALL-IMAGE ACCEPT, AT THE FINGER (Jon-ruled 2026-08-25) ──────────
   It opens in the rail, beside the control that opened the file dialog, so the
   customer answers where they are. The numbers sit under the warning in the
   caption size — they are facts, and they read as facts. */
.dz-toosmall[hidden] { display: none; }
.dz-toosmall {
  width: 100%; margin-top: 6px; padding: 10px;
  background: var(--pf-white); border: 1px solid var(--pf-orange);
  border-radius: var(--pf-ctl-r, 10px);
}
.dz-toosmall-w { margin: 0; font: 700 13px/1.3 Inter, system-ui, sans-serif; color: var(--pf-black); }
.dz-toosmall-n { margin: 4px 0 0; font: 400 11px/1.35 Inter, system-ui, sans-serif; color: var(--pf-muted, #6b6560); }
.dz-toosmall-n:empty { display: none; }
.dz-toosmall-row { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.dz-toosmall-row .btn { width: 100%; min-height: 44px; }

/* ══ MOBILE GETS THE SIDE RAILS, AND THEY RESERVE NOTHING (Jon-ruled
   2026-08-25) ═════════════════════════════════════════════════════════════
   "Tap opens, tap closes, comes right out, is super clear, and goes back in."
   MEASURED FIRST, at 390, which is why they reserve nothing: the stacked shape
   gave the flyer 293x411 in a section standing at 757 of a 758 screenful —
   right at the ceiling. Side rails that RESERVE NOTHING keep the picture at
   **293x411** and still take the section to **476**, because the stacked
   rails' own 281px of height goes away. Reserving a strip costs the picture
   instead: 20px each side gives 253x355, 28 gives 237x333, 36 gives 221x310,
   and desktop's 44 gives **205x288** — 30% of the flyer's linear size, on the
   viewport that can least afford it.
   So the rail is a TAB OVER THE FLYER'S OWN EDGE. It takes no room in the
   layout; it comes out over the picture when tapped and goes back in. */
@media (max-width: 1439px), (max-width: 1559px) and (min-height: 861px) {
  .dz-designrow {
    display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto;
    position: relative; padding-inline: 0; margin-inline: 0; gap: 8px;
  }
  .dz-designrow > #dz-gallery { grid-column: 1; grid-row: 1; }
  .dz-designrow > .dz-aibar { grid-column: 1; grid-row: 2; }
  /* ── THE RAIL IS BUILT INTO THE SECTION'S WALL (Jon-ruled 2026-08-26) ────
     "They read as pills laid on top. I want them built INTO the container's
     wall — seamlessly: squared against the wall it belongs to, no floating
     shadow, reading as part of the frame rather than something resting on the
     picture."

     MEASURED FIRST, at 390, because the previous shape was ruled on the
     arithmetic that a reserved strip costs the picture. It does — 20px each
     side takes the flyer to 246x349, 44 to 202x286. BUT NOTHING HAS TO BE
     RESERVED. The section already carries a channel the flyer does not use:

        x24  border 1px │ padding 16px │ sheet 9px │ WHITE FRAME 283px │ …
             └─────────── 25px of channel ───────────┘

     25px on the left (padding + the sheet's own margin), 32px on the right.
     The rail is absolutely positioned, so it takes NO layout room at all —
     pulled out into that channel it clears the picture entirely and the flyer
     does not move: 283x401 with the tab in place, 283x401 without it, measured
     both orientations.

     THE HEIGHT IS CLAMPED BY THE GRID, NOT BY A NUMBER. `grid-row: 1` on an
     absolutely-positioned grid child makes its containing block the GALLERY'S
     row, so `top/bottom: 0` can only ever span the picture. That is what stops
     it reaching the AI designer bar — structurally, rather than by a magic
     offset that the next layout change would silently break. Today's rails are
     sized to the whole row and cross the bar by 30x62 in landscape, the whole
     height of its button. */
  .dz-designrow > .dz-rail--looks,
  .dz-designrow > .dz-rail--controls {
    position: absolute; z-index: 6;
    /* BOTH LINES ARE EXPLICIT, AND THAT IS THE WHOLE FIX. For an absolutely
       positioned grid child an `auto` grid line does NOT mean "span one track"
       — it resolves to the grid container's PADDING EDGE. Written `grid-row: 1`
       the rail was placed from line 1 to the bottom of the row and measured
       510px against the gallery's 432, straight through the AI designer bar,
       while `grid-row-start` computed as `1` and looked correct. `1 / 2` is
       what actually confines it to the picture's track. */
    grid-column: 1 / 2; grid-row: 1 / 2;
    top: 0; bottom: 0;
    width: var(--pf-rail-rest, 25px); overflow: hidden;
    border: 0; border-radius: 0;
    background: var(--pf-cream, #FBF7F4);
    box-shadow: none;
    transition: width .16s ease-out;
    display: block;
  }
  /* out into the section's own padding, so the tab is flush to the orange
     border and the customer reads it as the frame, not as a card on the art */
  .dz-designrow > .dz-rail--looks {
    left: calc(-1 * var(--pf-rail-out, 16px));
    border-right: 1px solid var(--pf-border);
    border-radius: 0 8px 8px 0;
  }
  /* the controls tab starts BELOW the orange Edit tag: measured, the tag runs
     y253→269 while the gallery starts at 266, so it hangs 3px into the rail's
     top and today's tab clips it 27x3 */
  .dz-designrow > .dz-rail--controls {
    right: calc(-1 * var(--pf-rail-out, 16px));
    top: 8px;
    border-left: 1px solid var(--pf-border);
    border-radius: 8px 0 0 8px;
  }
  /* THE LABEL IS THE TAB (Jon-ruled 2026-08-26): "the resting tab must be
     plainly readable, not a whisper ... understated is my word, invisible is
     not." A cream body on a cream wall is what makes it read as built in — so
     the body carries the seam and the LABEL carries the legibility. It is set
     in the ink, not the muted grey: measured, the grey was 5.39:1 on cream and
     the ink is 15.30:1, both on the same ground. */
  .dz-designrow > .dz-rail::before {
    content: attr(data-rail-name);
    position: absolute; left: 50%; top: 10px;
    transform: translateX(-50%) rotate(180deg);
    writing-mode: vertical-rl; text-orientation: mixed;
    font: 700 11px/1 Inter, system-ui, sans-serif;
    letter-spacing: .11em; text-transform: uppercase;
    color: var(--pf-black, #1A1A1A);
    pointer-events: none;
  }
  .dz-designrow > .dz-rail > * { opacity: 0; transition: opacity .16s ease-out; }
  /* OUT OVER THE PICTURE on tap — as wide as its own words need (176 is what
     the labels measured at), or the room the screen has. */
  /* OPENING A MENU IS MEANT TO COVER THINGS (Jon-ruled 2026-08-26): the open
     panel MAY cover the flyer. It may never cover the AI designer bar and it
     may never leave the section — both of which the grid clamp above enforces
     by construction, since the panel cannot outgrow the gallery's row.
     The shadow goes: an open rail is the wall sliding back, not a card landing
     on the picture. */
  .dz-designrow > .dz-rail[data-open="1"] {
    width: min(192px, calc(100vw - 90px));
    overflow-y: auto; overflow-x: hidden;
    box-shadow: none;
    background: var(--pf-white);
    padding: 6px;
  }
  .dz-designrow > .dz-rail--looks[data-open="1"] { border-radius: 0 10px 10px 0; }
  .dz-designrow > .dz-rail--controls[data-open="1"] { border-radius: 10px 0 0 10px; }
  /* ── THE TAB STAYS WHEN THE RAIL COMES OUT (Jon's real-phone walk,
     2026-08-25: "tap opens, tap closes ... comes right out, is super clear,
     and goes back in") ────────────────────────────────────────────────────
     Walked with a finger: the second tap did NOT close it, because an open
     rail is controls edge to edge and a finger has nowhere to land that is
     not a control. The name strip stays on the OUTER edge while the panel is
     out, so the thing that opened it is still there to press — and the panel
     is inset past it so no control sits under the strip. */
  .dz-designrow > .dz-rail[data-open="1"]::before {
    top: 50%; transform: translateX(-50%) translateY(-50%) rotate(180deg);
    color: var(--pf-muted, #6b6560);
  }
  .dz-designrow > .dz-rail--looks[data-open="1"]::before { left: 12px; }
  .dz-designrow > .dz-rail--controls[data-open="1"]::before { left: auto; right: 12px; }
  .dz-designrow > .dz-rail--looks[data-open="1"] { padding-left: 25px; }
  .dz-designrow > .dz-rail--controls[data-open="1"] { padding-right: 25px; }
  /* the strip itself is the close: it is the rail's own body, so the tap
     handler reads it as "not a control" and sends the panel back in */
  .dz-designrow > .dz-rail[data-open="1"]::after {
    content: ""; position: absolute; top: 0; bottom: 0; width: 25px; z-index: 1;
  }
  .dz-designrow > .dz-rail--looks[data-open="1"]::after { left: 0; }
  .dz-designrow > .dz-rail--controls[data-open="1"]::after { right: 0; }
  .dz-designrow > .dz-rail[data-open="1"] > * { opacity: 1; }
  .dz-designrow > .dz-rail--looks[data-open="1"] { width: min(112px, calc(100vw - 90px)); }
  /* stacked, the two-state controls stand their halves in a column too, so a
     176px rail holds "Landscape" and "Confirm this design" whole */
  /* THE PANEL IS AS WIDE AS THE COLUMNS IT ACTUALLY NEEDS (2026-08-26).
     Clamping the height to the picture made this visible: the open rail is a
     WRAPPING flex column, so eight 44px controls in a 264px landscape track
     wrap into a second column — and at a fixed 192px the second column landed
     OUTSIDE the panel, where `overflow: hidden` ate it. "Confirm this design"
     was not clipped, it was GONE, and every vertical measurement said the
     content fitted, because column-wrap grows the cross axis.
     `fit-content` makes the width follow the wrap instead of fighting it: one
     column in portrait, two in landscape, where there are 558px of unused
     width beside a flyer that is capped by height anyway. */
  /* NOWRAP IS THE DEFAULT, because a wrapped column leaves the panel.
     Portrait wrapped even at 432px of track: an auto-margin spacer separates
     Reset and Confirm from the rest and measured EIGHTY pixels, so 420px of
     controls needed 500. With nowrap the spacer collapses, the eight controls
     take 8x44 + 7x8 + 12 = 420, and they fit. */
  .dz-designrow > .dz-rail--controls[data-open="1"] {
    display: flex; flex-direction: column; flex-wrap: nowrap; gap: 8px;
  }
  .dz-designrow > .dz-rail--controls[data-open="1"] .seg[data-two] { display: flex; flex-direction: column; gap: 4px; }
  .dz-designrow > .dz-rail--controls[data-open="1"] .seg[data-two] > button { width: 100%; justify-content: flex-start; }
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-lab { overflow: visible; text-overflow: clip; }
}

/* ══ THE RAIL IS A COLUMN WHEREVER IT IS A RAIL (Jon's real-phone walk,
   2026-08-25) ═════════════════════════════════════════════════════════════
   "The left rail does not work. It opens to arrows and a bare strip with no
    thumbnails."
   MEASURED AT 390 WITH THE RAIL OPEN AT 96px:
     .dz-car   grid-template-columns: 44px 0px 44px
     .dz-tpl-row  4px wide at x96
     6 thumbs at 56px each, drawn x98 → x278, in a rail that ends at x137
   Two arrows and a ZERO-WIDTH middle column, with the pictures painted
   outside the rail and clipped away. It is the same `.dz-car`-is-a-grid fault
   that was found and fixed for the desktop rail on 2026-08-25 — and the fix
   was written INSIDE the three-column media block, so when the handset got
   floating rails this morning it inherited the rail and not the fix.
   THE CAUSE IS THAT THESE RULES WERE SCOPED BY VIEWPORT WHEN THE THING THEY
   DESCRIBE IS A SHAPE. A rail is a narrow column at 390 and at 1600 alike, so
   the rules are scoped to the rail itself and follow it wherever it goes. */
.dz-designrow > .dz-rail--looks .dz-tpl-row {
  flex-direction: column; flex-wrap: nowrap; width: 100%; flex: 1 1 auto;
  overflow-x: hidden; overflow-y: auto; max-height: 100%;
  align-items: center; gap: 6px; scroll-snap-type: y proximity;
}
.dz-designrow > .dz-rail--looks .dz-tpl-row > button { width: 100%; }
.dz-designrow > .dz-rail--looks .dz-car {
  display: grid; grid-template-columns: 1fr; grid-auto-flow: row; gap: 4px;
}
.dz-designrow > .dz-rail--looks .dz-car-arw { width: 100%; height: 26px; }
.dz-designrow > .dz-rail--looks .dz-tpl-row > button > img { width: 62px; height: auto; }
.dz-designrow > .dz-rail--looks .dz-tpl-row > button > span {
  max-width: 62px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  display: block; line-height: 14px;
}
/* AND THE RAIL IS TALL ENOUGH TO BE A LIST. At rest it is a tab; open it is a
   column of pictures, and a column of pictures that can only show one is not a
   picker. It takes the gallery's height, and scrolls inside it. */
.dz-designrow > .dz-rail--looks[data-open="1"] { max-height: 100%; }

/* ── THE AI DESIGNER IS A BAR UNDER THE STAGE, ON BOTH VIEWPORTS (Jon-ruled
   2026-08-25, superseding this morning's arrangement branch) ───────────────
   "It goes where it is on desktop — at the bottom, opening downward."
   It had been moved into the control cluster wherever the parts were stacked,
   and this morning the handset's controls became a 30px TAB — so the AI
   designer stopped being on the page at all until the rail was tapped. A
   control that has to be found before it can be used is not on the page. */
.dz-aibar { width: 100%; grid-column: 1; }
.dz-aibar > .dz-ai { width: 100%; }
.dz-aibar .dz-ai-btn { width: 100%; min-height: 44px; }

/* ── THE GHOST IS A CROSSED GLYPH, AND NOTHING ELSE (Jon-ruled 2026-08-25) ──
   "The landscape reason text goes — one small understated line, or nothing at
    all with just the crossed glyph. Jon's preference is no text."
   The cross is drawn on the GLYPH, not on the label: a struck-out word reads
   as something that was chosen and then dropped, which is what the earlier
   line-through got wrong. A crossed picture of a landscape page reads as
   "not this shape", which is the truth. */
.dz-orient-wrap[data-locked="1"] button[data-v="landscape"] .dz-g {
  position: relative; opacity: .55;
}
.dz-orient-wrap[data-locked="1"] button[data-v="landscape"] {
  position: relative;
}
.dz-orient-wrap[data-locked="1"] button[data-v="landscape"] .dz-g::after {
  content: ""; position: absolute; left: -2px; right: -2px; top: 50%;
  height: 1.5px; background: var(--pf-muted, #6b6560);
  transform: rotate(-24deg); transform-origin: 50% 50%;
}
/* the glyph is an <svg>, which cannot carry ::after — the cross is drawn on a
   wrapper the button already has, positioned over the glyph's own box */
.dz-orient-wrap[data-locked="1"] button[data-v="landscape"]::after {
  content: ""; position: absolute; left: 8px; width: 21px; top: 50%;
  height: 1.5px; background: var(--pf-muted, #6b6560);
  transform: rotate(-24deg); pointer-events: none;
}
.dz-designrow > .dz-rail--controls[data-open="1"] .dz-orient-wrap[data-locked="1"] button[data-v="landscape"]::after {
  left: 10px;
}

/* ── THE CURSOR SAYS WHAT THE THING IS (Jon-ruled 2026-08-25) ──────────────
   "An arrow on hover, a text cursor over a text band, nothing special over an
    image."
   What it replaces: `cursor: zoom-in` on the ring and its tag — the
   magnifying glass Jon met over a control that, at the time, did nothing. A
   magnifying glass promises magnification; the ring opens an EDITOR. */
.dz-ring, .dz-ring-tag { cursor: pointer; }
.dz-face-prev, .dz-face-prev img, .dz-zoom-face img { cursor: default; }
/* a TEXT band takes the text cursor; a band that holds a picture does not */
.dz-zoom .dz-regions [data-region] { cursor: text; }
.dz-zoom .dz-regions [data-region][data-holds="asset"],
.dz-face-prev #dz-regions [data-region][data-holds="asset"] { cursor: default; }

/* ── THE DESKTOP RAILS OPEN WIDER (Jon-ruled 2026-08-25: "there is room — but
   stay tidy and lined up") ─────────────────────────────────────────────────
   176 was the narrowest that held every label whole; it is not the widest the
   deck can spare. At 1600 the design section is 685 and the picture is 465, so
   each side has room. 210 gives every label its full width with air after it,
   and the controls stay left-aligned to one edge so the column reads as a list
   rather than a drift. The picture does not move: the rails are out of flow. */
@media (min-width: 1560px), (min-width: 1440px) and (max-height: 860px) {
  .dz-designrow > .dz-rail--controls:hover,
  .dz-designrow > .dz-rail--controls:focus-within,
  .dz-designrow > .dz-rail--controls[data-open="1"] { width: 210px; padding: 6px; }
  .dz-designrow > .dz-rail--looks:hover,
  .dz-designrow > .dz-rail--looks:focus-within,
  .dz-designrow > .dz-rail--looks[data-open="1"] { width: 120px; padding: 6px; }
  .dz-designrow > .dz-rail--looks[data-open="1"] .dz-tpl-row > button > img,
  .dz-designrow > .dz-rail--looks:hover .dz-tpl-row > button > img { width: 84px; }
  .dz-designrow > .dz-rail--looks[data-open="1"] .dz-tpl-row > button > span,
  .dz-designrow > .dz-rail--looks:hover .dz-tpl-row > button > span { max-width: 84px; }
  /* LINED UP: one left edge for every control in the open rail */
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-lab,
  .dz-designrow > .dz-rail--controls:hover .dz-lab { text-align: left; }
}

/* ══ AT REST, A RAIL IS ITS TAB AND NOTHING ELSE (Jon's second phone walk,
   2026-08-25) ═════════════════════════════════════════════════════════════
   "Today four control icons stand over the flyer before anything is opened.
    Nothing but the tab shows until the customer opens it — both rails, both
    viewports."
   MEASURED AT REST: the looks rail showed 16 elements and the controls 12 at
   1600, and 16 and 19 at 390 — arrows, buttons, thumbnails and glyphs, all
   standing over the picture in a 30-44px strip. The desktop rest rule only
   ever hid the LABELS, which left every glyph on the glass; the handset put
   `opacity: 0` on the rail's direct children, which is a paint trick a
   screen reader still reads and a finger can still hit.
   `visibility: hidden` is the honest one: it inherits to every descendant, it
   takes the children out of the tab order, and it makes them untappable — so
   a resting rail is a tab, not a transparent panel. The tab itself is drawn
   by `::before`, which is not a child and so is not hidden with them. */
.dz-designrow > .dz-rail:not(:hover):not(:focus-within):not([data-open="1"]) > * {
  visibility: hidden;
}
.dz-designrow > .dz-rail:not(:hover):not(:focus-within):not([data-open="1"]) {
  overflow: hidden;
}

/* ── THE LEFTOVER CAROUSEL ARROW (Jon's second phone walk, item 3) ─────────
   "It is the old carousel's control inside a column. Remove it."
   Measured: 2 of them, both viewports, at the top of a rail that scrolls on
   its own axis. They step a HORIZONTAL strip that no longer exists here — the
   thumbs are a column now, and a column scrolls. A control that does nothing
   is worse than no control, and this build has ruled that twice. */
.dz-designrow > .dz-rail--looks .dz-car-arw { display: none; }

/* ── OPEN, A RAIL IS AS BIG AS ITS WORDS NEED (Jon's second phone walk,
   item 2) ─────────────────────────────────────────────────────────────────
   "It floats over the picture, so width while open costs nothing."
   MEASURED, on the handset, where it was worst:
     the templates rail opened to 96 — 6 of padding and 30 of tab leave 60 of
     content — and "Classic frame" wants 71 and "Simple frame" 69, so both
     clipped;
     the controls rail opened to 176, leaving exactly 140, and the longest
     label wants exactly 140 — no clip by a single pixel, which is not a
     margin, it is a coincidence.
   So each is opened to its widest word plus its own furniture, and then some
   air: it costs the picture nothing, because an open rail floats. */
@media (max-width: 1439px), (max-width: 1559px) and (min-height: 861px) {
  .dz-designrow > .dz-rail--looks[data-open="1"] { width: min(124px, calc(100vw - 80px)); }
  .dz-designrow > .dz-rail--controls[data-open="1"] { width: min(212px, calc(100vw - 60px)); }
  .dz-designrow > .dz-rail--looks[data-open="1"] .dz-tpl-row > button > img { width: 76px; }
  .dz-designrow > .dz-rail--looks[data-open="1"] .dz-tpl-row > button > span { max-width: 82px; }
}

/* ══ LANDSCAPE'S OPEN PANEL IS PARKED, AND THIS IS WHY (2026-08-26) ═══════
   Jon: "Every control in it fully visible — Confirm this design must not
   clip." PORTRAIT MEETS THAT: nowrap, 8 controls x 44 + 7 gaps x 8 + 12 =
   420 in a 432px track, gated on all four edges.

   LANDSCAPE CANNOT, IN ONE COLUMN. A 844x390 handset gives the picture's
   track 264px, and eight controls need 420. Below 44px they stop being tap
   targets, so the only honest arrangement is TWO COLUMNS — and that is a
   layout change, not a width. Three attempts are on record and all three
   failed for the same reason, which is the useful part:

     · width 192 + wrap  → the second column landed outside the panel and
       `overflow: hidden` ate it. "Confirm this design" was not clipped, it
       was GONE, and every vertical measure said the content fitted.
     · width: fit-content → for a column-wrap flex container the max-content
       width is the WIDEST ITEM, not the sum of the columns. Panel stayed 211.
     · width 404 + wrap  → the items are `width: 100%`, so widening the panel
       widened THEM; three controls sat 355px and 735px past the edge.

   THE NEXT PRECISE STEP: the flex items are NOT the rail's direct children.
   `.dz-primary` and `.dz-under-block` carry `display: contents`, so the items
   are their children — which is exactly what the block below this one was
   written to reach. Extend THAT selector list under a short-viewport query
   with `flex: 0 0 calc(50% - 4px)`, and the 404px panel holds two columns.

   UNTIL THEN THE PANEL DOES NOT WRAP, so nothing is clipped away and every
   control is reachable by scrolling — reachable beats invisible. The gate
   stays RED on landscape's "fully visible" rather than being softened to
   match, because the requirement is Jon's and it is not met yet. */

/* ══ THE CONTROL SYSTEM REACHES THE PANEL'S CHILDREN (Jon-ruled 2026-08-25) ══
   "One gap, one width, filling the box, and dz-primary measuring at 390."
   MEASURED IN THE OPEN PANEL BEFORE THIS: gaps of 0, then 6, then 8 — three
   values where the system declares one; children **108 and 69 wide inside a
   140px content box** on the handset, so they neither filled it nor lined up
   on the right; and `dz-primary` not measuring at 390 at all.
   THE CAUSE IS THE SAME SHAPE AS THE CAROUSEL'S: the system was written for
   the desktop ROW and its rules stopped at the panel's own border. A rail is a
   COLUMN OF ONE-WIDTH ROWS wherever it opens, so that is what is declared —
   scoped to the open rail itself, not to a viewport. */
.dz-designrow > .dz-rail--controls:hover,
.dz-designrow > .dz-rail--controls:focus-within,
.dz-designrow > .dz-rail--controls[data-open="1"] {
  display: flex; flex-direction: column;
  gap: var(--pf-ctl-gap, 8px);
  align-items: stretch;
}
/* every direct child is one row of the column, at the panel's own width */
.dz-designrow > .dz-rail--controls:hover > *,
.dz-designrow > .dz-rail--controls:focus-within > *,
.dz-designrow > .dz-rail--controls[data-open="1"] > * {
  width: 100%; min-width: 0; margin: 0; flex: 0 0 auto;
}
/* `.dz-primary` and the artwork rows are WRAPPERS, not rows: `display:
   contents` hands their children straight to the column, which is what makes
   `dz-primary` measure — it had no box of its own to be measured at 390. */
.dz-designrow > .dz-rail--controls:hover .dz-primary,
.dz-designrow > .dz-rail--controls:focus-within .dz-primary,
.dz-designrow > .dz-rail--controls[data-open="1"] .dz-primary,
.dz-designrow > .dz-rail--controls:hover .dz-under-block[data-under="solo"],
.dz-designrow > .dz-rail--controls:focus-within .dz-under-block[data-under="solo"],
.dz-designrow > .dz-rail--controls[data-open="1"] .dz-under-block[data-under="solo"],
.dz-designrow > .dz-rail--controls:hover .dz-under-block[data-under="card"],
.dz-designrow > .dz-rail--controls:focus-within .dz-under-block[data-under="card"],
.dz-designrow > .dz-rail--controls[data-open="1"] .dz-under-block[data-under="card"] {
  display: contents;
}
/* which makes THEIR children rows of the same column, at the same width */
.dz-designrow > .dz-rail--controls:hover .dz-primary > *,
.dz-designrow > .dz-rail--controls:focus-within .dz-primary > *,
.dz-designrow > .dz-rail--controls[data-open="1"] .dz-primary > *,
.dz-designrow > .dz-rail--controls:hover .dz-under-block > *,
.dz-designrow > .dz-rail--controls:focus-within .dz-under-block > *,
.dz-designrow > .dz-rail--controls[data-open="1"] .dz-under-block > * {
  width: 100%; min-width: 0; margin: 0; flex: 0 0 auto;
}
/* a two-state control is one row that holds two, and it fills the row */
.dz-designrow > .dz-rail--controls:hover .seg,
.dz-designrow > .dz-rail--controls:focus-within .seg,
.dz-designrow > .dz-rail--controls[data-open="1"] .seg { width: 100%; }
/* captions and notes span the column and never take a control's height */
.dz-designrow > .dz-rail--controls[data-open="1"] .upload-note,
.dz-designrow > .dz-rail--controls[data-open="1"] .dz-waitfields { width: 100%; }

/* ══ A SHORT TRACK TAKES TWO COLUMNS (Jon-ruled 2026-08-26, built 2026-08-27)
   ═══════════════════════════════════════════════════════════════════════════
   "Every control in it fully visible — Confirm this design must not clip."
   PORTRAIT meets that in one column: 8 x 44 + 7 x 8 + 12 = 420 in a 432 track.
   LANDSCAPE CANNOT — a 844x390 handset gives the picture's track 264px, and
   below 44px a control stops being a tap target. So the panel wraps into two
   columns and is made wide enough to show both.

   THIS IS WHERE THE THREE EARLIER ATTEMPTS DIED, AND WHY THIS ONE WORKS.
   Widening the panel alone widened the ITEMS with it, because the rule above
   sets `> *` to `width: 100%` — three controls sat 355px and 735px past the
   edge of a 403px panel. And `width: fit-content` cannot help: for a
   column-wrap flex container the max-content width is the WIDEST ITEM, not
   the sum of its columns.
   THE ITEMS ARE NOT THE RAIL'S CHILDREN. `.dz-primary` and `.dz-under-block`
   carry `display: contents`, so the flex items are THEIR children — which is
   exactly what the block above reaches. The half-width rule follows the same
   path, or it lands on wrappers that have no box.

   MEASURED: a column holds floor((264 - 12) / 52) = 4 controls, so eight take
   two. Landscape has 558px of width sitting unused beside a flyer that is
   capped by height, so this costs the picture nothing.
   THE NUMBER IS NOT THE GUARANTEE — gate-rails-wall.js measures every control
   against the panel's client box on all four edges and asserts "Confirm this
   design" is RENDERED, so a ninth control turns it red rather than clipping. */
@media (max-height: 560px) {
  .dz-designrow > .dz-rail--controls[data-open="1"] {
    width: min(404px, calc(100vw - 90px));
    flex-wrap: wrap;
  }
  .dz-designrow > .dz-rail--controls[data-open="1"] > *,
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-primary > *,
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-under-block > *,
  /* `.seg` IS NAMED EXPLICITLY, and that is the last thing that was wrong.
     `[data-open="1"] .seg { width: 100% }` is a FOUR-selector rule; the `> *`
     above is three, so it lost on specificity however late it was written.
     The two-state control stayed 372px wide, and a column-wrap flex container
     takes its column width from its WIDEST item — so one control set a 372px
     column, the eight wrapped into THREE of them, and 403px of panel could
     never hold that. Same specificity now, and later in the file. */
  .dz-designrow > .dz-rail--controls[data-open="1"] .seg,
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-orient-wrap {
    width: calc(50% - 4px);
  }
  /* AND NOTHING GROWS DOWN THE COLUMN. `#dk-solo-art` carries
     `flex: 1 1 148px` — in a COLUMN that grows in HEIGHT, and it measured
     182x200 where every other control is 44. It was not a gap between Upload
     and Reset; it was the Upload control eating 164px of the column, which
     pushed "Confirm this design" into a third column the panel is not wide
     enough to show. In a wrapped column a growing item is a column-eater. */
  .dz-designrow > .dz-rail--controls[data-open="1"] #dk-solo-art,
  .dz-designrow > .dz-rail--controls[data-open="1"] #dk-card-art,
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-primary > *,
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-under-block > * { flex: 0 0 auto; }
  /* the full-width exceptions stay full-width: a note is not a control */
  .dz-designrow > .dz-rail--controls[data-open="1"] .upload-note,
  .dz-designrow > .dz-rail--controls[data-open="1"] .dz-waitfields { width: 100%; }
}
.dz-designrow > .dz-rail--controls[data-open="1"] .upload-note:empty { display: none; }

/* ══ THE SHARED ARTEFACT SHRINKS WHILE NOTHING IS CHOSEN (Jon-ruled
   2026-08-25, option 2 — BOTH LAWS STAND) ═════════════════════════════════
   The artefact-at-every-breakpoint law and the deck layout law are untouched:
   the sheet is still the artefact, still the centre, still tappable, still the
   thing that explains Shared to someone who has never seen it. It is simply
   SMALLER while there is nothing of the customer's on it — and full size the
   moment a space is held.

   THE NUMBER IS 280, AND IT IS THE SAME 280 AS THE SAMPLE'S, deliberately:
   one number for one idea, already ratified for this exact state on
   2026-08-24. Measured on the handset at 390x844:

     | hero height | artefact | SPEC on screen |
     | today (424) | 300x424  | 229px          |
     | 360         | 255x360  | 293px          |
     | **280**     | **198x280** | **373px**   |
     | 240         | 170x240  | 392px          |
     | 200         | 141x200  | 392px — no further gain |

   280 buys back **144 of the 163px** that are there to win, and below 240 the
   deck's own layout stops giving anything at all while the cells go on
   shrinking toward strips. THE HANDSET ONLY: on the deck the artefact has
   never been in the customer's way. */
@media (max-width: 900px) {
  /* THE WIDTH IS WHAT IS SET, because the height is DERIVED from it: the hero
     carries `aspect-ratio: 210 / 297`, so a height cap fights the ratio and the
     first attempt collapsed the sheet inside to 196x140. 198 of width IS 280 of
     height on an A4 ratio, and everything within follows without being told
     anything — which is the measure-never-infer law read the other way round:
     set the one dimension the box actually derives from. */
  [data-deck][data-pf-held="0"] #dk-hero-art {
    width: 198px; max-width: 100%; margin-inline: auto;
  }

  /* ── AND THE FLYER PLACEHOLDERS STAND DOWN (Jon-ruled 2026-08-25) ────────
     "They are the apparatus for a step not reached."
     A front and a back standing empty above a grid the customer has not
     touched yet is furniture for work that cannot start — and on Shared they
     stack over the artefact, which is what Jon walked into. They come back
     with the hold, because that is the moment the work exists. */
  [data-deck][data-pf-held="0"][data-deck="card"] .dz-face-block[data-face="front"],
  [data-deck][data-pf-held="0"][data-deck="card"] .dz-face-block[data-face="back"] {
    display: none;
  }
}

/* ══ WHY THE LOOK RAIL'S COLUMN RULE WAS DEAD, AND WHAT KILLED IT ═════════
   Jon, 2026-08-27: "only one template appears." The rule that makes the rail a
   scrolling column has existed since 2026-08-25 at line 7242 and was IN THE
   SERVED BYTES — and Chrome did not have it.

   THE CAUSE WAS A STRAY `}` AT LINE 7224, AND IT WAS MINE: left behind in
   30969780 when the `@media (max-height: 560px)` block was relocated. A stray
   closing brace is a parse error, and Chrome recovers by discarding until it
   finds a valid rule start — which ATE THE NEXT RULE. Exactly one: removing
   the brace took the shortfall from 23 to 22 and the rule came back.

   FOUND BY BISECTION, after three instruments lied about it in a row:
   matching selector TEXT flagged every grouped selector; `CSS.supports(
   'selector(...)')` rejects comma lists and flagged forty valid rules; and a
   per-sheet `catch` swallowed a cross-origin SecurityError, which I read as
   "the rule is absent". What settled it was injecting PREFIXES of the file and
   watching where the parsed count stopped tracking the text count — a question
   with no name-matching in it at all. The rule survived alone and died with
   the forty lines before it; the brace was in those forty lines.

   No re-declaration is needed now. The original rule at 7242 is live. */

/* ══ A MIRROR DROPS THE PRINT IT CANNOT SHOW (Jon-ruled 2026-08-27: "a strange
   image showing 32 sits at the top") ═══════════════════════════════════════
   MEASURED on Shared at 390: `#dk-hero-thumb` is a 196x278 tile holding a
   HALF-SCALE sheet — `matrix(0.5026)` — and inside it `.pfsh-count` renders
   "0 of 32 taken" at 9px, which is **4.5px on screen**, measured 27x5. Jon read
   it as a stray image because at that size it is not words, it is texture.
   THE COUNTER IS NOT LOST: it is stated properly below the tile at 55x11 and
   again in the rail. What goes is a copy nobody can read.
   The rule is general on purpose — a mirror shows the shape of the thing, and
   drops the small print it cannot render legibly, rather than printing it at a
   size that reads as dirt. */
#dk-hero-thumb .pfsh-count,
#dk-hero-thumb .pfsh-small,
#dk-hero-thumb .small { display: none; }

/* ══ A PICTURE GLYPH WITH A PLUS, SO A CUSTOMER KNOWS AN IMAGE GOES THERE
   (Jon-ruled 2026-08-24, again 2026-08-26, built 2026-08-27) ═══════════════
   IT DID NOT FAIL TO LAND — IT LANDED AS COPY. `.pff-empty` exists on both
   faces, 256x83, and renders a SENTENCE: "Your design lands here — use the
   Designer or upload your art". `background-image: none`, `::before: none`.
   No glyph was ever drawn, so the ruling read as done in the code and was
   absent on the screen.
   THE MARK IS DRAWN HERE, as a mark: an inline SVG frame with a mountain, a
   lens and a PLUS, in the brand orange, on the region that HOLDS AN IMAGE —
   `[data-region="image-slot"]` and anything declaring `data-holds="asset"`,
   so it follows the rule rather than one named box. It shows only while the
   slot is EMPTY: a slot with the customer's picture in it needs no invitation.
   Gated on the drawn mark, never on the element existing —
   `gate-image-glyph.js` reads the computed `::after` and its box. */
/* AND THE ORANGE PANEL IS THE ONE JON MEANT. Measured in the edit view:
   `[data-region="hero"]` is 447x130, EMPTY — no children, no text — tinted
   rgba(253,54,1,.07), and it sits exactly on the dashed rectangle inside the
   orange band. That dashed rectangle is baked into the look's own artwork, so
   there was nothing in the DOM there to carry a mark; the region is. */
/* THE MARK IS DRAWN IN INK, NOT IN A FIXED COLOUR — and the reason is a
   green gate over a bare panel. The first build painted the glyph in brand
   orange as a background-image; on the flyer's ORANGE BAND that is orange on
   orange, and the crop capture is a flat rectangle. Every assertion passed:
   the ::after had content, it carried an IMAGE, the image contained the plus,
   it had a 48x44 box. Not one of them could have failed for the colour.
   A MASK makes the colour a token the surface sets, so a mark on the orange
   panel is white and a mark on the cream slot is ink; and the gate now reads
   the RENDERED PIXELS, which is the assertion that would have gone red. */
[data-region="hero"]:empty::after,
[data-region="image-slot"]:empty::after,
[data-region][data-holds="asset"]:empty::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: min(46%, 48px);
  aspect-ratio: 48 / 44;
  --pf-glyph-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 44'%3E%3Crect x='1.5' y='5.5' width='36' height='30' rx='3.5' fill='none' stroke='%23000' stroke-width='3'/%3E%3Ccircle cx='12' cy='15' r='3.5' fill='%23000'/%3E%3Cpath d='M4 32 L15 21 L23 28 L29 23 L35 30' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M39 26 V40 M32 33 H46' stroke='%23000' stroke-width='3.5' stroke-linecap='round'/%3E%3C/svg%3E");
  -webkit-mask-image: var(--pf-glyph-mark);
          mask-image: var(--pf-glyph-mark);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center;   mask-position: center;
  -webkit-mask-size: contain;      mask-size: contain;
  background-color: var(--pf-glyph-ink, #FD3601);
  pointer-events: none;
}
/* the orange panel is the one Jon named, and it is the one where brand orange
   disappears — its ink is white */
[data-region="hero"]:empty::after { --pf-glyph-ink: #FFFFFF; }

[data-region="hero"],
[data-region="image-slot"],
[data-region][data-holds="asset"] { position: relative; }





/* ── THE CELLS-OWN-THE-PRESS RULES ARE RETIRED WITH THE MECHANISM ──────────
   (Jon-ruled 2026-08-27.) They raised the sheet's cells above the flyer
   pictures so a tap would land on a cell rather than on the artwork. There is
   no longer a tap to land: the shape cards are the choice at every width and
   the sheet is a sample. Keeping stacking or pointer-events overrides for a
   chooser that no longer exists is exactly the orphan the ruling removes. */

/* ══════════ AN UPLOAD IS NOT EDITABLE (Jon-ruled 2026-08-29) ══════════════
   "Locked and reasoned, THE SAME TREATMENT AS THE LANDSCAPE LOCK — not
    offered, muted, with the reason beneath it."
   So this is deliberately the landscape lock's own declarations, on the band
   targets instead of the orientation control: muted ink, muted border, the
   not-allowed cursor, and no hover or pressed treatment whatever it is told.
   The band targets are transparent hit areas over the artwork, so the muting
   that reads there is the OUTLINE and the cursor — a grey fill would cover
   the customer's own flyer, which is the thing the lock exists to protect. */
.dz-regions[data-locked="1"] button[data-region] {
  cursor: not-allowed;
  outline: 1px dashed var(--pf-grey-200, #eee);
  outline-offset: -2px;
}
.dz-regions[data-locked="1"] button[data-region]:hover,
.dz-regions[data-locked="1"] button[data-region]:focus-visible,
.dz-regions[data-locked="1"] button[data-region][aria-pressed="true"] {
  outline-color: var(--pf-grey-200, #eee);
  background: transparent;
}
/* the reason reads exactly as the landscape one does, in the same ink */
.dz-edit-why {
  margin: 4px 0 0; font: 400 11px/1.35 Inter, system-ui, sans-serif;
  color: var(--pf-muted, #6b6560);
}
.dz-edit-why[hidden] { display: none; }
/* IT ONLY SPEAKS WHERE IT APPLIES. The sentence is about upload mode, so it is
   shown when the bands are locked and stands down otherwise — never `hidden`
   in the locked state, because hidden is not announced. */
.dz-regions:not([data-locked="1"]) + .dz-edit-why { display: none; }

/* ── THE ORIENTATION PAIR IS A READOUT (build 3, Jon-ruled 2026-08-27) ────
   Lit and not pressable. It reports which way round the FACE ON SCREEN is,
   measured from the picture — so it must not look like something to press:
   no pointer, no hover lift, and the unlit half recedes rather than sitting
   there inviting a tap. `square` and `unknown` deliberately light NEITHER
   half; their words are Jon’s and are not invented here. */
.dz-orient-wrap[data-readout='1'] .dz-orient button { cursor: default; }
.dz-orient-wrap[data-readout='1'] .dz-orient button:hover,
.dz-orient-wrap[data-readout='1'] .dz-orient button:focus-visible { transform: none; }
.dz-orient-wrap[data-readout='1'] .dz-orient button[aria-pressed='false'] { opacity: .45; }

/* the readout names the source of its answer — a caption, never a control */
.dz-orient-note { margin: .28rem 0 0; font-size: .72rem; line-height: 1.3; opacity: .72; }

/* ══ BANDS OPEN ALONGSIDE EACH OTHER (Jon-ruled 2026-08-29) ═══════════════
   TRANSPARENCY IS PART OF THIS BUILD, NOT AFTER IT (his ruling): 'two boxes
   that overlap and cannot both be read is the same feature failing quietly'.
   Bands can sit 40px apart, so two open boxes WILL overlap. The one being
   typed in comes forward and is fully opaque; the others stay readable
   through a frosted ground rather than being hidden behind it. Opacity alone
   was wrong here — it makes both sets of words muddy instead of putting one
   in front. */
.dz-editor.dz-ed-box { z-index: 4; }
.dz-editor.dz-ed-box:not(:focus-within) {
  background: rgba(255, 255, 255, .84);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  box-shadow: 0 1px 6px rgba(0, 0, 0, .16);
}
.dz-editor.dz-ed-box:focus-within {
  z-index: 7;
  background: var(--pf-paper, #fff);
  box-shadow: 0 4px 18px rgba(0, 0, 0, .26);
}
@media (prefers-reduced-transparency: reduce) {
  .dz-editor.dz-ed-box:not(:focus-within) { background: var(--pf-paper, #fff); -webkit-backdrop-filter: none; backdrop-filter: none; }
}
/* at 390 every box stays OPEN and one is visible — the deck hides the rest
   with [hidden], so nothing here has to know which */

/* ══ THE EDITOR COLUMN, BESIDE THE FACE (Jon-ruled 2026-08-29) ═══════════
   Stacked in the order they were opened, never over the flyer and never over
   each other. Positioned by the deck, which measures the face — a CSS-only
   guess at where the face is would be wrong the moment it is zoomed or
   panned. */
.dz-ed-col {
  position: absolute; z-index: 8;
  display: flex; flex-direction: column; gap: 10px;
  overflow-y: auto; overscroll-behavior: contain;
}
.dz-ed-col[hidden] { display: none; }
.dz-editor.in-column {
  position: static; width: 100%; max-width: none;
  top: auto; left: auto; transform: none;
}
/* TRANSPARENCY STAYS BUT STOPS BEING LOAD-BEARING (Jon-ruled 2026-08-29):
   it is polish for the resting boxes now, not the thing making the feature
   work. Nothing overlaps in the column, so this only softens the boxes that
   are not being typed in. */
.dz-ed-col .dz-editor:not(:focus-within) { opacity: .92; }

/* ══ A BAND HOLDING UNSAVED WORDS (Jon-ruled 2026-08-29) ═════════════════
   The empty-picture mechanism, one band along: the same attribute pattern,
   its own mark, and the chip dropping on a band too small to carry one.
   The chip is Jon’s word and it is TEXT, not a glyph - `Unsaved` says what a
   picture of a pencil only implies. A dotted edge distinguishes it from the
   dashed edge of an empty picture slot without inventing a second colour. */
.dz-regions button[data-draft='1']:not([data-empty='1']) {
  border-style: dotted; border-color: var(--pf-orange, #FD3601);
  background: rgba(253, 54, 1, .14);
}
.dz-regions button[data-draft='1']:not([data-empty='1'])::after {
  content: 'Unsaved';
  /* TOP LEFT, opposite the edit control (Jon-ruled 2026-08-30). Same text,
     same dotted edge, nothing else changes - the mark and the way in do not
     compete for the same corner. */
  position: absolute; left: 4px; top: 4px;
  padding: 2px 6px; border-radius: 999px;
  font: 600 10px/1.35 Inter, system-ui, sans-serif; letter-spacing: .02em;
  color: #fff; background: var(--pf-orange, #FD3601);
  /* THE CHIP CARRIES ITS OWN OUTLINE (Jon-ruled 2026-08-30), so it holds on
     any ground rather than needing a rule per colour. Orange on orange was
     the case in front of us - on the headline band the chip’s fill matched
     the band and only the lettering separated it - and the next look will
     bring another. One treatment: a white ring outside the fill, and the
     shadow beneath it for a dark ground. */
  box-shadow: 0 0 0 1.5px #fff, 0 1px 3px rgba(0, 0, 0, .3);
  pointer-events: none; white-space: nowrap;
}
/* below about 26px there is no room for a chip, exactly as the empty mark
   already rules - the dotted brand edge is the whole message there */
.dz-regions button[data-draft='1'].dz-rg-small::after { display: none; }

/* ── UPLOAD MODE STATES ONE VALUE (Jon-ruled 2026-08-30) ─────────────────
   Not two halves with one lit: the file already decided, so there is nothing
   to offer and nothing to refuse. It reads as a fact with its source
   beneath, and there is no second half to press. */
.dz-orient-wrap[data-single='1'] .dz-orient { border: 0; background: none; padding: 0; }
.dz-orient-wrap[data-single='1'] .dz-orient button[data-v] {
  cursor: default; opacity: 1; background: none; border: 0; padding: 0;
}
.dz-orient-wrap[data-single='1'] .dz-orient button[data-v]:hover,
.dz-orient-wrap[data-single='1'] .dz-orient button[data-v]:focus-visible { transform: none; background: none; }
