/* app.css — 🔴 THE ONE STYLESHEET. The design tokens and the studio frame of afshin's approved v5 look.
 *
 * ==================================================================================================
 * ONE TOKEN SOURCE, ONE FILE, NO THEMES (C-11)
 * ==================================================================================================
 * V1 shipped SIX looks of six product pages — Midnight, Daylight, Copper, Forest, Sand, Sea Glass —
 * which is SIX stylesheets each re-declaring the same seventeen tokens, plus thirty generated page
 * twins on top of the six dark sources. Six spellings of one palette is guaranteed drift — the
 * "identical frame" that was never identical. (MEASURED 2026-08-09, and the count this comment carried
 * until then was "five themes = thirty stylesheets": five is the number of GENERATED twins, not of
 * looks, and V1 has never shipped thirty stylesheets — the live deploy folder and all three frozen
 * releases each hold exactly six.) V2 has ONE
 * stylesheet for every product and every surface, and it is this one. There is no theme picker, no
 * per-product sheet and no `.mirror-` prefix anywhere: a product changes what the SERVER sends, never
 * what the browser is styled with. A second stylesheet appearing in the shipped bundle reds the build
 * (`stylesheet-fanout` in tools/frontend-bundle-guard.ts) — that is the FAN-OUT regression made
 * mechanically impossible rather than merely discouraged. (It was called "the thirty-file regression"
 * until 2026-08-09; a name carrying a number is a name that can be wrong, and that one was — the rule
 * counts SHEETS and thirty was the count of generated PAGE twins.)
 *
 * The palette, the typography and the geometry below are read off the approved v5 mockup. The layout
 * numbers it fixes — the 84px rail gutter, the 200px inspector, the 58px topbar, the 64px head — are
 * CUSTOM PROPERTIES here rather than literals in JavaScript, which keeps them in one place AND keeps
 * them out of the bundle guard's arithmetic rules, where geometry beside a coefficient is unreadable
 * from a price formula.
 *
 * 🔴 THERE IS NO PRICE IN A STYLESHEET, AND NO FIGURE PAINTED FROM ONE. Not a currency symbol, not a
 * money-named custom property (`--price: 900` is the one shape in which CSS can hand a number to code
 * that computes), and not one `content:` declaration carrying a digit — CSS-generated text puts a
 * figure on the screen and creates NO DOM node, so the behavioural walk in app-smoke is blind to it and
 * the bundle guard's `css-content-figure` rule is that walk's §67 pair. Every `content:` below is `""`.
 *
 * NOT IN THIS PASS: the mobile portrait sheet, the zoom cluster, the landscape three-zone bonus. They
 * are Stage 5, they adapt THIS file by CSS alone, and there will be no second page and no second sheet.
 *
 * Standing Rule 5 (Rule Zero): written fresh from the spec; no line copied from any previous version. */

:root {
  /* ---- the aqua-glass palette (v5, LOOK only) ---- */
  --navy: #0c4a6f;
  --navy-rgb: 12, 74, 111;
  --blue: #147da8;
  --sky: #36bce7;
  --ice: #e9f7fc;
  --paper: #f7fafc;
  --white: #fff;
  --ink: #10283a;
  --muted: #60798b;
  --faint: #89a0af;
  --deep: #0c3551;
  --line: rgba(12, 74, 111, .14);
  --line-soft: rgba(12, 74, 111, .075);
  --line-strong: rgba(12, 74, 111, .26);
  --ok: #1f9e6f;
  /* B1 — the ONE red on this page, and the only colour afshin's 2026-07-31 ruling introduced. It is a
     TOKEN rather than a hex typed at the two rules that use it (RULE #1/C-11): a second spelling of a
     colour is how "identical" started drifting in V1. It is named for its MEANING, not its hue, so a
     later palette change moves one line. Nothing else on this page is red. */
  --danger: #c0392b;
  --handle: #f47a24;
  --shadow: 0 24px 70px rgba(26, 76, 106, .12);
  --shadow-soft: 0 8px 22px rgba(12, 74, 111, .06);
  --sans: "Avenir Next", "Segoe UI", Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;

  /* ---- the studio's geometry, named once (v5's own measurements) ---- */
  --topbar-h: 58px;
  --head-h: 64px;
  --rail-gutter: 84px;
  --rail-w: 66px;
  --tool-w: 54px;
  --tool-h: 58px;
  --inspector-w: 200px;
  --studio-min-h: 620px;
  --radius-lg: 22px;
  --radius: 15px;
  --radius-sm: 12px;
}

* { box-sizing: border-box; }

/* ==================================================================================================
   🔴 `hidden` MEANS HIDDEN. THE ONE VISIBILITY SWITCH MUST BEAT EVERY LAYOUT RULE IN THIS FILE.
   ==================================================================================================
   `ui-kit.js`'s `show()` sets the `hidden` ATTRIBUTE and nothing else — "the one visibility switch, so
   a surface cannot be hidden two ways". The browser's own `[hidden] { display: none }` is a USER-AGENT
   rule, which any author `display` beats. So every class in this file that sets `display` silently
   overrode the switch, and a surface the document called hidden stayed on the screen.

   🔴 MEASURED, on the real dev server, 2026-08-04, before this line existed. `#resultChoices` — the two
   choices "Edit your request" and "Submit your request for further review" — carried `hidden=true` and
   computed `display: flex` at 622x46 px, ON A SUCCESSFUL PRICED QUOTE — a real mirror job that came
   back with a real total and nine priced lines. (The figure itself is deliberately NOT written here:
   this file states no amount, and a money literal is no less a money literal for sitting in a comment.
   It is in the lane record, `docs/qa/VERIFY-2026-08-04-hidden-attribute.md`, and in the test below.)
   A customer who had just been given a price was being invited to submit the job for manual review.
   It was on the site-offline page too (622x46 at y=800, above the fold), which is where it was first
   spotted — but the priced road is the one nobody was looking at.

   🔴 WHY GLOBAL AND WHY `!important`, RATHER THAN `.result-actions[hidden]`. The one-class repair is
   what `admin.css` already did for `.palette`, and it left every OTHER class in that file exposed —
   three of which were live defects on the sign-in door the day this was written. Measured on the priced
   page: 66 of 204 elements would have ignored `hidden` had anyone set it. That is the CLASS; the two
   buttons were one INSTANCE of it (§94). `[hidden]` has the specificity of a single class, so a rule
   without `!important` merely wins by ORDER — and the next `display` declaration appended below it
   reopens the hole with nothing to say so. `!important` makes the switch position-independent.

   🔴 AND IT IS FREE, CHECKED RATHER THAN ASSUMED. This rule can only reach elements matching `[hidden]`.
   Measured on the priced road: 15 elements carried `hidden`, 14 already computed `none`, and the 15th
   is the defect above. Nothing in this bundle sets an inline `style="display:..."` and no other
   declaration in either sheet is `!important` (both asserted in
   test/frontend/hidden-attribute.invariant.test.ts), so there is nothing for this to fight with.

   The invariant is held by that test across EVERY stylesheet the deployment uploads, enumerated from
   the bundle rather than named here — a third sheet tomorrow is covered without an edit. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  font-family: var(--sans);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

body { background: #e7edf1; }

button, input, select { font: inherit; color: inherit; }

/* ---- the shell backdrop: a soft radial glow and two glass blobs ------------------------------- */

.shell-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  background:
    radial-gradient(96% 72% at 50% 24%, rgba(181, 229, 246, .24) 0%, rgba(181, 229, 246, .09) 45%, transparent 82%),
    linear-gradient(180deg, #fbfdff 0%, #f3f8fb 100%);
}

.shell-bg::before,
.shell-bg::after { content: ""; position: absolute; }

.shell-bg::before {
  width: 520px;
  height: 740px;
  left: -265px;
  top: -170px;
  border: 1px solid rgba(var(--navy-rgb), .06);
  border-radius: 42% 58% 47% 53%;
  transform: rotate(24deg);
  background: linear-gradient(135deg, rgba(255, 255, 255, .68), rgba(200, 235, 247, .09));
}

.shell-bg::after {
  width: 640px;
  height: 640px;
  right: -360px;
  bottom: -260px;
  border: 1px solid rgba(var(--navy-rgb), .07);
  border-radius: 50%;
  background: radial-gradient(circle at 25% 25%, rgba(255, 255, 255, .9), rgba(195, 232, 246, .08) 60%, transparent 72%);
}

.app { position: relative; z-index: 1; min-height: 100vh; display: flex; flex-direction: column; }

/* ---- ZONE 0 · the topbar ---------------------------------------------------------------------- */

.topbar {
  flex: 0 0 auto;
  height: var(--topbar-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  border-bottom: 1px solid rgba(12, 74, 111, .08);
  background: rgba(248, 252, 254, .88);
  backdrop-filter: blur(16px);
}

.brand { display: flex; align-items: center; gap: 11px; text-decoration: none; color: var(--navy); }

/* the brand mark is DRAWN in CSS — two clipped triangles, the v5 glyph. No image, no request. */
.mark {
  position: relative;
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: 11px;
  background: linear-gradient(145deg, #0f628c, #1ab1dc);
  box-shadow: 0 10px 22px rgba(20, 125, 168, .16);
}

.mark::before,
.mark::after { content: ""; position: absolute; clip-path: polygon(50% 0, 0 100%, 100% 100%); background: #fff; }
.mark::before { width: 12px; height: 11px; left: 6px; top: 9px; }
.mark::after { width: 7px; height: 6px; right: 5px; top: 6px; }

.brand-copy b { display: block; font-size: 14px; letter-spacing: .16em; line-height: 1; }

.brand-copy small {
  display: block;
  margin-top: 3px;
  font-size: 8.5px;
  text-transform: uppercase;
  letter-spacing: .24em;
  color: var(--muted);
}

.top-center { display: flex; align-items: center; gap: 10px; }
.pname { font-size: 13px; font-weight: 700; color: var(--deep); }

.back-link {
  display: flex;
  align-items: center;
  height: 34px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: var(--muted);
  font-size: 12px;
  background: rgba(255, 255, 255, .72);
}

/* ---- the AI composer · Stage 6 · afshin's approved v5 composer, restored in THIS file's tokens ------
 * 🔴 SURFACE LOOK ONLY, from the designer package (§76 — a mood board, not a spec): the glass rail, the
 * focus glow, the bordered AI glyph with its waiting shards, the prompt and the gradient "Ask" button.
 * Every colour, radius and shadow below is an EXISTING `--token` declared above — nothing new is
 * introduced, so the composer cannot drift away from the studio it sits over (C-11). No `content:`
 * carries a digit and there is no <canvas>. The photo button is afshin's own `.photo`, restored now that
 * its `/api/ai` door exists (SECURITY F6) — a fourth grid column between the prompt and the submit, every
 * colour/radius below an EXISTING `--token`. It has no preview strip, gallery or remove control: those are
 * appearance beyond his approved composer (§75) and are not added here. */
.ai-stage {
  flex: 0 0 auto;
  display: grid;
  justify-items: center;
  padding: 20px 20px 6px;
}

.composer-wrap { position: relative; width: min(720px, 100%); }

.composer-glow {
  position: absolute;
  inset: -24px -50px;
  z-index: -1;
  background: radial-gradient(ellipse, rgba(54, 188, 231, .18), transparent 68%);
  filter: blur(10px);
  opacity: .72;
  transition: .35s;
}

.composer-wrap:focus-within .composer-glow { opacity: 1; transform: scale(1.03); }

.composer {
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 8px;
  padding: 8px 9px 8px 8px;
  border: 1px solid rgba(var(--navy-rgb), .20);
  border-radius: var(--radius-lg);
  background: rgba(255, 255, 255, .82);
  box-shadow: var(--shadow), inset 0 1px 0 #fff;
  backdrop-filter: blur(22px) saturate(125%);
  transition: .28s;
}

.composer:focus-within {
  border-color: rgba(20, 125, 168, .48);
  box-shadow: 0 28px 85px rgba(26, 76, 106, .16), 0 0 0 5px rgba(54, 188, 231, .08), inset 0 1px #fff;
}

.ai-glyph {
  position: relative;
  width: 34px;
  height: 34px;
  justify-self: center;
  border: 1px solid rgba(var(--navy-rgb), .16);
  border-radius: 11px;
  background: linear-gradient(145deg, rgba(255, 255, 255, .94), rgba(211, 240, 250, .58));
  display: grid;
  place-items: center;
  overflow: hidden;
  box-shadow: inset 0 1px #fff, 0 8px 18px rgba(28, 97, 130, .08);
}

.ai-glyph svg { width: 22px; height: 22px; overflow: visible; }

.shard-a, .shard-b, .shard-c { transform-origin: center; animation: waitShard 3.6s ease-in-out infinite; }
.shard-b { animation-delay: -1.2s; }
.shard-c { animation-delay: -2.4s; }

@keyframes waitShard {
  0%, 100% { transform: translate(0, 0); }
  45% { transform: translate(1.3px, -1.8px); }
  70% { transform: translate(-.8px, .8px); }
}

/* the "thinking" state — app.js sets it on the form while a turn is in flight, and clears it after. */
.composer.thinking .ai-glyph { animation: glyphThink .8s ease-in-out infinite alternate; }
.composer.thinking .shard-a { animation-duration: .72s; }
.composer.thinking .shard-b { animation-duration: .91s; }
.composer.thinking .shard-c { animation-duration: 1.1s; }

@keyframes glyphThink {
  to { box-shadow: 0 0 0 8px rgba(54, 188, 231, .10), 0 10px 24px rgba(28, 97, 130, .12); }
}

.prompt-box { position: relative; min-width: 0; text-align: left; }

.prompt-label {
  display: block;
  min-height: 10px;
  font-size: 8px;
  font-weight: 800;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--blue);
}

.prompt {
  width: 100%;
  height: 22px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-size: 14px;
  outline: 0;
}

.prompt::placeholder { color: var(--faint); opacity: 1; }

.submit {
  height: 38px;
  min-width: 96px;
  border: 0;
  border-radius: var(--radius-sm);
  padding: 0 14px;
  background: linear-gradient(135deg, var(--navy), var(--blue));
  color: #fff;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .02em;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  box-shadow: 0 12px 28px rgba(var(--navy-rgb), .20);
  transition: .2s;
}

.submit:hover { transform: translateY(-1px); box-shadow: 0 16px 32px rgba(var(--navy-rgb), .26); }
.submit:disabled { opacity: .6; cursor: progress; transform: none; }
.submit svg { width: 15px; height: 15px; }

/* the photo button — afshin's own `.photo`, restored in this file's tokens. A quiet transparent glyph
   that lifts to the ice wash on hover, so it never competes with the gradient "Ask". Colour and radius are
   EXISTING tokens (`--muted`, `--ice`, `--blue`, `--radius-sm`), so it cannot drift from the composer. */
.photo {
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: .2s;
}

.photo:hover { background: var(--ice); color: var(--blue); }
.photo svg { width: 22px; height: 22px; }

.composer-note {
  margin: 7px 0 0;
  color: var(--faint);
  font-size: 11px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
}

.composer-note i { width: 6px; height: 6px; border-radius: 50%; background: var(--sky); }

/* the persona sentence — one per line, appended into the chat overlay's thread (`.chat-thread`). No figure
   ever lives here: every claim is server-composed copy the honesty-gate cleared, painted through the one
   wire door. Its old inline home on the studio (`.ai-answer`) is gone with afshin's THREAD-VIEW move. */
.ai-claim {
  margin: 0;
  padding: 12px 15px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, .72);
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink);
}

/* mobile: the composer reflows to two rows (Stage 5 adapts THIS file by CSS alone — no second sheet). The
   photo takes the bottom-left cell and the submit fills the rest of the bottom row — afshin's own mobile
   arrangement, in this file's columns. */
@media (max-width: 700px) {
  .composer {
    grid-template-columns: 44px 1fr;
    grid-template-rows: auto auto;
    gap: 10px;
  }
  .ai-glyph { grid-column: 1; grid-row: 1; }
  .prompt-box { grid-column: 2; grid-row: 1; }
  .prompt { font-size: 16px; }
  .photo { grid-column: 1; grid-row: 2; width: 44px; height: 46px; }
  .submit { grid-column: 2; grid-row: 2; width: 100%; height: 46px; }
}

/* ---- the attached-photo chip · SECURITY F6 · §75-approved OPTION 1 --------------------------------
 * A TEXT + inline-glyph confirmation on the composer that a photo is attached to the NEXT ask — NEVER a
 * pixel preview (a customer image surface is refused by the frontend price wall). Every colour/radius is
 * an EXISTING --token (C-11); `--ok` for the check glyph is the same green the studio already declares. */
.photo-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  width: fit-content;
  max-width: 100%;
  margin: 9px auto 0;
  padding: 7px 8px 7px 13px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgba(255, 255, 255, .82);
  color: var(--navy);
  font-size: 12px;
  font-weight: 700;
}

.photo-chip-glyph { width: 15px; height: 15px; color: var(--ok); flex: 0 0 auto; }

.photo-chip-remove {
  width: 22px;
  height: 22px;
  border: 0;
  border-radius: 50%;
  background: var(--ice);
  color: var(--muted);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
}

.photo-chip-remove:hover { background: var(--line); color: var(--danger); }

/* ---- the not-listed OPTIONAL photo picker · afshin's F1 control, §75-OPTION-1 ----------------------
 * afshin's compact "choose a photo" affordance from V1's `custom-request`, restored INLINE in the Others
 * capture and wired to POST /lead-photo. The whole label opens the device picker (it wraps the hidden file
 * input); a chosen picture is uploaded and CONFIRMED by the `.photo-chip` above — TEXT + a check glyph,
 * never a pixel preview, the customer image surface the price wall refuses. Every colour/radius is an
 * EXISTING --token (C-11), so it cannot drift from the rest of the page, and the class overrides the bare
 * `label` rule above rather than inheriting its tiny caption chrome. */
.photo-pick {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 8px 14px;
  border: 1px dashed rgba(var(--navy-rgb), .30);
  border-radius: var(--radius-sm);
  background: var(--paper);
  color: var(--blue);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0;
  text-transform: none;
  cursor: pointer;
  width: fit-content;
  max-width: 100%;
}

.photo-pick:hover { background: var(--ice); }
.photo-pick svg { width: 18px; height: 18px; flex: 0 0 auto; }
.photo-pick em { color: var(--muted); font-style: normal; font-weight: 500; }

/* the upload status line — an honest inline sentence when a photo cannot be attached (an unsupported
   image, an over-rate caller, an unbound store). The photo is OPTIONAL, so every such road leaves the
   customer able to submit text-only; this note never blocks the form. It paints no figure. */
.photo-note { margin: 8px 0 0; }

/* ---- the photo modal · SECURITY F6 · afshin's own #photoModal, §75-approved OPTION 1 --------------
 * His modal LOOK, every colour/radius/shadow an EXISTING --token (C-11), SKIPPING his `.preview` <img>:
 * a customer image surface collides with the frontend price wall and is a separate post-Friday lane, so
 * this modal confirms an attached photo in TEXT + an inline check glyph only — never a pixel preview. It
 * carries no <canvas> and no `content:` digit. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: 20px;
  background: rgba(var(--navy-rgb), .20);
  backdrop-filter: blur(12px);
  opacity: 0;
  pointer-events: none;
  transition: .25s;
}

.modal.open { opacity: 1; pointer-events: auto; }

/* the backdrop sits under the card and fills the overlay; a click on it dismisses. Transparent, since the
   overlay's own tint is on `.modal`. */
.modal-backdrop {
  position: absolute;
  inset: 0;
  border: 0;
  padding: 0;
  background: transparent;
  cursor: default;
}

.modal-card {
  position: relative;
  z-index: 1;
  width: min(520px, 100%);
  border: 1px solid rgba(var(--navy-rgb), .17);
  border-radius: var(--radius-lg);
  background: var(--white);
  padding: 24px;
  box-shadow: var(--shadow);
  text-align: left;
}

.modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 16px; }
.modal-head small { font-size: 10px; letter-spacing: .13em; text-transform: uppercase; font-weight: 800; color: var(--blue); }
.modal-head h3 { margin: 5px 0 0; font-size: 22px; letter-spacing: -.02em; color: var(--ink); }

.modal-close {
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--white);
  color: var(--muted);
  font-size: 18px;
  cursor: pointer;
}

.modal-close:hover { background: var(--ice); color: var(--blue); }

/* the pick surface — afshin's `.drop`, WITHOUT his `.preview` <img>. Two TEXT states (idle / attached),
   toggled by app.js; the input is hidden and the whole label opens the device picker natively. */
.drop {
  display: grid;
  place-items: center;
  text-align: center;
  min-height: 168px;
  padding: 22px;
  border: 1px dashed rgba(var(--navy-rgb), .30);
  border-radius: var(--radius);
  background: var(--paper);
  cursor: pointer;
}

.drop input { display: none; }
.drop svg { width: 34px; height: 34px; margin-bottom: 9px; color: var(--blue); }
.drop b { display: block; font-size: 15px; color: var(--ink); }
.drop p { max-width: 340px; margin: 7px auto 0; font-size: 12px; line-height: 1.5; color: var(--muted); }
.drop-done svg { color: var(--ok); }

.modal-actions { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-top: 16px; }
.modal-actions span { font-size: 11px; color: var(--faint); }

.analyze {
  height: 42px;
  flex: 0 0 auto;
  border: 0;
  border-radius: var(--radius-sm);
  padding: 0 18px;
  background: linear-gradient(135deg, var(--navy), var(--blue));
  color: #fff;
  font-size: 12px;
  font-weight: 800;
  cursor: pointer;
}

.analyze:disabled { opacity: .35; cursor: not-allowed; }

/* ---- the salesperson chat overlay · §75 (afshin's THREAD-VIEW + CHAT OVERLAY) --------------------
 * A dismissible overlay OVER the studio: the page dims and shows through behind a chat card that holds a
 * scrolling transcript, minimize + close controls, and a continuation input. Its dim-behind look reuses
 * the `.modal` idiom above — every colour/radius/shadow is an EXISTING app.css --token (C-11), so it can
 * never drift from the rest of the page. Mobile-first: the card is a near-full-height sheet on a phone
 * and a centred dialog on a wide screen. It carries no <img>, no image `src`, no `content:` digit and no
 * brand literal — the header name is the server-filled `data-tenant-name`, the words in the thread are
 * the server's. */
.chat-overlay {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: grid;
  place-items: end center;
  padding: 0;
  background: rgba(var(--navy-rgb), .22);
  backdrop-filter: blur(12px);
  opacity: 0;
  pointer-events: none;
  transition: .25s;
}

.chat-overlay.open { opacity: 1; pointer-events: auto; }

/* the backdrop fills the overlay under the card; a click on it minimizes. Transparent — the tint is on
   `.chat-overlay` itself, exactly as the photo modal splits them. */
.chat-backdrop {
  position: absolute;
  inset: 0;
  border: 0;
  padding: 0;
  background: transparent;
  cursor: default;
}

.chat-card {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 88vh;
  max-height: 88vh;
  border: 1px solid rgba(var(--navy-rgb), .17);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: var(--white);
  box-shadow: var(--shadow);
  text-align: left;
  overflow: hidden;
}

.chat-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, rgba(211, 240, 250, .40), transparent);
}

.chat-head-id { display: grid; gap: 3px; min-width: 0; }

.chat-kicker {
  min-height: 10px;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--blue);
}

.chat-sub { font-size: 12px; color: var(--muted); }

.chat-controls { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }

.chat-min,
.chat-close {
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--white);
  color: var(--muted);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  display: grid;
  place-items: center;
}

.chat-min svg { width: 16px; height: 16px; }
.chat-min:hover,
.chat-close:hover { background: var(--ice); color: var(--blue); }

/* the transcript — grows to fill the card and scrolls, so the history stays visible as space allows. */
.chat-thread {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px 18px;
  background: var(--paper);
}

/* one turn's group of persona lines. Aligned to the start (the salesperson's side); a customer side is a
   deferred, price-wall-gated addition, so only the agent side is styled today. */
.chat-turn { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; max-width: 92%; }

.chat-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 14px;
  border-top: 1px solid var(--line);
  background: var(--white);
}

.chat-input {
  flex: 1 1 auto;
  min-width: 0;
  height: 42px;
  padding: 0 14px;
  border: 1px solid rgba(var(--navy-rgb), .20);
  border-radius: 999px;
  background: rgba(255, 255, 255, .82);
  color: var(--ink);
  font-size: 14px;
  outline: 0;
  transition: .2s;
}

.chat-input:focus { border-color: rgba(20, 125, 168, .48); box-shadow: 0 0 0 4px rgba(54, 188, 231, .08); }
.chat-input::placeholder { color: var(--faint); opacity: 1; }

.chat-send {
  width: 42px;
  height: 42px;
  flex: 0 0 auto;
  border: 0;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--navy), var(--blue));
  color: #fff;
  cursor: pointer;
  display: grid;
  place-items: center;
  box-shadow: 0 12px 28px rgba(var(--navy-rgb), .20);
  transition: .2s;
}

.chat-send:hover { transform: translateY(-1px); }
.chat-send:disabled { opacity: .6; cursor: progress; transform: none; }
.chat-send svg { width: 16px; height: 16px; }

/* a wide screen: the sheet becomes a centred dialog rather than a full-height bottom sheet. */
@media (min-width: 720px) {
  .chat-overlay { place-items: center; padding: 24px; }
  .chat-card {
    width: min(560px, 100%);
    height: min(680px, 82vh);
    max-height: 82vh;
    border-radius: var(--radius-lg);
  }
}

/* a phone: fill the input for a comfortable touch target and keep the type size above the zoom threshold. */
@media (max-width: 700px) {
  .chat-card { height: 92vh; max-height: 92vh; }
  .chat-input { font-size: 16px; }
}

/* ---- the studio: head + three zones ------------------------------------------------------------ */

.studio { position: relative; flex: 1 1 auto; min-height: var(--studio-min-h); overflow: hidden; }

.head {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  z-index: 18;
  height: var(--head-h);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 0 20px;
  border-bottom: 1px solid rgba(12, 74, 111, .07);
  background: linear-gradient(180deg, rgba(248, 252, 254, .92), rgba(248, 252, 254, .58));
  backdrop-filter: blur(12px);
}

.head-label {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--faint);
}

.head-change { justify-self: center; text-align: center; min-width: 280px; }
.head-change strong { font-size: 16px; color: var(--deep); }
.head-change small { display: block; margin-top: 2px; font-size: 11px; color: var(--muted); }

.head-total { justify-self: end; text-align: right; }

.head-total span {
  display: block;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--faint);
}

.head-total strong {
  display: block;
  font-size: 24px;
  line-height: 1.05;
  letter-spacing: -.04em;
  color: var(--deep);
}

/* ZONE 2 · the canvas. The gutters are the rail's and the inspector's, so the drawing owns the rest. */
.canvas {
  position: absolute;
  inset: var(--head-h) var(--inspector-w) 0 var(--rail-gutter);
  display: grid;
  place-items: center;
  overflow: hidden;
}

.canvas::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(0deg, transparent 0 46px, rgba(12, 74, 111, .025) 46px 47px),
    repeating-linear-gradient(90deg, transparent 0 46px, rgba(12, 74, 111, .025) 46px 47px);
}

/* 🔴 THE PIECE — Stage 4's live drawing, and the WHOLE of its appearance.
 *
 * `preview-render.js` builds an `<svg viewBox="0 0 w h">` holding ONE outline and writes not a single
 * colour, size or pixel: every visible property of the drawing is here, in the one stylesheet, because
 * a look spelled in two places is how "identical" started drifting in V1 (C-11).
 *
 * The SIZE is a share of the zone rather than a computed fit. The `viewBox` carries the customer's own
 * two answers unchanged and `preserveAspectRatio` does the scaling, so the browser fits the piece and
 * the bundle does no arithmetic to keep it on screen — and the percentage below is what leaves it air
 * on all four sides instead of touching the gutters.
 *
 * `vector-effect: non-scaling-stroke` is why there is one stroke width for every job. Without it the
 * stroke is scaled by the viewBox mapping, so a 12-inch piece would draw a heavy outline and a
 * 96-inch one a hairline — the same piece looking like two different products.
 *
 * COLOURS ARE TOKENS, NOT HEX. Nothing new is introduced: the outline is the palette's `--blue` and
 * the glass is the same `--navy-rgb` wash the frame already uses. */
.piece {
  width: 78%;
  height: 78%;
}

.piece-outline {
  fill: rgba(var(--navy-rgb), .07);
  stroke: var(--blue);
  stroke-width: 1.5px;
  vector-effect: non-scaling-stroke;
}

/* 🔴 v0.15 (c2) — THE CLASH RED. An opening geometry-check.js flags (off the sheet, or too close to
 * another) carries `.piece-clash` ALONGSIDE `.piece-outline`, so its geometry is unchanged and only its
 * colour turns red. It reuses the EXISTING `--danger` token — the SAME red an invalid form field already
 * carries (`input[aria-invalid="true"]` below) — so NO new colour or token is introduced. Placed after
 * `.piece-outline` so its `stroke` wins at equal specificity.
 *
 * 🔴 THE COLOUR IS THE FUNCTIONAL DEFAULT, FLAGGED FOR afshin's APPEARANCE BATCH (§75), exactly as C1's
 * brand-blue openings and the shower band are. The MECHANISM (a flagged opening reads red, drawn == the
 * server's own refusal) builds behind that; the exact shade is his to confirm. */
.piece-clash {
  stroke: var(--danger);
}

/* 🔴 v0.13 C1-a — THE ESTIMATE CARD'S DRAWING. afshin reserved this slot (B3) between the heading and
 * the priced lines; `paintCardDrawing` fills it with the SAME `.piece` outline the studio canvas draws.
 *
 * The studio's `.piece` sizes at 78% of the absolutely-filled canvas ZONE; the card slot has no such
 * zone, so the slot is given a bounded, self-contained band here and the outline fits inside it by the
 * same `preserveAspectRatio` the studio uses — the bundle does no arithmetic to keep it on screen. The
 * band is capped so the picture never pushes the price and specs below it out of view.
 *
 * 🔴 NO NEW COLOUR OR TOKEN: the outline reuses `.piece-outline` (the palette's `--blue` and the
 * `--navy-rgb` wash), the same appearance the studio drawing already carries. The SIZE below is a
 * conservative default, not a final look — the one appearance knob still open (§75). */
#resultDrawing {
  display: grid;
  place-items: center;
  height: 160px;
  margin: 4px 0 14px;
}
#resultDrawing .piece {
  width: 100%;
  height: 100%;
}

/* 🔴 v0.15 (c1-b) — THE RETURN PAGE'S LIVE-DESIGN DRAWING. The same `.piece` outline the studio canvas and
 * the estimate card draw, on the re-opened estimate. It reuses `.piece` / `.piece-outline` (no new colour or
 * token) and the same bounded, self-contained band + `preserveAspectRatio` the card slot uses, so the
 * picture fits without the bundle doing any arithmetic. The SIZE is a conservative default, not a final
 * look — the one appearance knob still open (§75), riding the same opened-estimate approval as the page. */
#estimateDrawing {
  display: grid;
  place-items: center;
  height: 170px;
  margin: 4px 0 16px;
}
#estimateDrawing .piece {
  width: 100%;
  height: 100%;
}

/* ZONE 1 · the tool rail. Empty until Stage 2 renders it from the server's `steps[]`. */
.tool-rail {
  position: absolute;
  z-index: 22;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  gap: 4px;
  width: var(--rail-w);
  padding: 7px 6px;
  border-radius: 20px;
  border: 1px solid rgba(12, 74, 111, .07);
  background: rgba(247, 251, 253, .56);
  backdrop-filter: blur(14px);
}

.tool-rail:empty { border-color: transparent; background: transparent; backdrop-filter: none; }

.tool {
  position: relative;
  width: var(--tool-w);
  height: var(--tool-h);
  display: grid;
  place-items: center;
  align-content: center;
  gap: 3px;
  border: 0;
  border-radius: var(--radius);
  background: rgba(247, 251, 253, .86);
  color: var(--muted);
  box-shadow: 0 1px 0 rgba(255, 255, 255, .9);
}

.tool b { font-size: 8px; letter-spacing: .04em; }
.tool-step { position: absolute; left: 6px; top: 5px; font-size: 7px; font-weight: 900; letter-spacing: .08em; color: var(--faint); }

.tool::after {
  content: "";
  position: absolute;
  right: 6px;
  top: 7px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: rgba(12, 74, 111, .12);
  box-shadow: 0 0 0 3px rgba(12, 74, 111, .035);
}

.tool.done::after { background: var(--ok); box-shadow: 0 0 0 4px rgba(31, 158, 111, .12); }
.tool.unlocked:not(.done)::after { background: var(--sky); box-shadow: 0 0 0 4px rgba(54, 188, 231, .13), 0 0 14px rgba(54, 188, 231, .32); }

/* 🔴 §75 shape-flow — DIM + LOCK a step the customer has not reached yet (slice 7). afshin: "بقیه
 * آیتم‌ها همه غیرقابل‌انتخاب و کمرنگ … اینطوری که الان هست مشتری گیج می‌شه." The rung already read
 * dimmed; this makes the "not yet" EXPLICIT by drawing the universal padlock in the status-dot corner,
 * so a locked step reads as deliberately-closed rather than merely faint. PURE PRESENTATION on the
 * SERVER-DERIVED `locked` state (`stepper.deriveStates` — `unlocked(k) = unlocked(k-1) AND
 * satisfied(k-1)`): no new state, no JS, no digit, and a locked rung's click is already a no-op
 * (`aria-disabled`). The dim is lifted slightly (.38 → .5) ONLY so the lock mark itself stays legible
 * through the rung's own opacity; the rung is still plainly backgrounded against the sky-lit unlocked
 * and green done rungs. Existing tokens only. A locked rung is never `active`, so its `::before`
 * (the shackle) can never collide with `.tool.active::before`. */
.tool.locked { opacity: .5; filter: saturate(.5); }

.tool.locked::after {            /* the padlock BODY — replaces the resting status dot on a locked rung */
  background: rgba(var(--navy-rgb), .7);
  box-shadow: none;
  width: 9px;
  height: 7px;
  right: 6px;
  top: 9px;
  border-radius: 2px;
}

.tool.locked::before {           /* the padlock SHACKLE — a small open arc sitting above the body */
  content: "";
  position: absolute;
  right: 8px;
  top: 4px;
  width: 5px;
  height: 5px;
  border: 1.5px solid rgba(var(--navy-rgb), .7);
  border-bottom: 0;
  border-radius: 3px 3px 0 0;
  background: transparent;
}

.tool.active {
  background: rgba(255, 255, 255, .98);
  color: var(--blue);
  box-shadow: 0 12px 28px rgba(20, 125, 168, .12);
  transform: translateX(2px);
}

.tool.active::before {
  content: "";
  position: absolute;
  left: -7px;
  top: 11px;
  bottom: 11px;
  width: 3px;
  border-radius: 3px;
  background: linear-gradient(180deg, var(--sky), var(--blue));
}

/* ZONE 3 · the inspector. */
.inspector {
  position: absolute;
  z-index: 20;
  right: 0;
  top: var(--head-h);
  bottom: 0;
  width: var(--inspector-w);
  padding: 22px 13px 18px 18px;
  overflow: auto;
  border-left: 1px solid rgba(12, 74, 111, .055);
  background: linear-gradient(90deg, rgba(246, 250, 252, 0), rgba(246, 250, 252, .62) 16%, rgba(246, 250, 252, .96) 36%);
  backdrop-filter: blur(8px);
}

.insp-kicker {
  margin-bottom: 6px;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--blue);
}

.insp-head b { display: block; font-size: 18px; line-height: 1.08; letter-spacing: -.03em; color: var(--deep); }
.insp-head small { display: block; margin-top: 6px; font-size: 12px; line-height: 1.45; color: var(--muted); }
.insp-content { margin-top: 16px; }

.insp-action {
  display: grid;
  gap: 7px;
  margin-top: 18px;
  padding-top: 13px;
  border-top: 1px solid rgba(12, 74, 111, .08);
}

.complete-btn {
  height: 40px;
  border: 0;
  border-radius: var(--radius-sm);
  background: linear-gradient(135deg, var(--navy), var(--blue));
  color: #fff;
  font-size: 11px;
  font-weight: 900;
  letter-spacing: .02em;
  box-shadow: 0 11px 24px rgba(12, 74, 111, .15);
  cursor: pointer;
}

.complete-btn:disabled { opacity: .55; cursor: progress; }
.step-note { font-size: 12px; line-height: 1.4; color: var(--muted); }

/* ---- the exit door (P27 · L2 place-2 — the per-form "not listed" escape hatch) ------------------
   A quiet link under the inspector, present on EVERY step so a customer whose item is not among the
   questions always has a road out of the form (afshin L2/A11). Pure chrome — no price, no digit, and
   no colour that duplicates a token (every colour is an EXISTING token, RULE #1/C-11). Its appearance
   is §75 build-then-review (DEC-295 §75-don't-wait). */
.exit-door { margin: 12px 0 0; text-align: center; }
.exit-door-link {
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  text-decoration: none;
  border-bottom: 1px dashed var(--line-strong);
  padding-bottom: 1px;
}
.exit-door-link:hover { color: var(--blue); border-bottom-color: var(--blue); }

/* ---- the trade-identify capture (Pro-C Slice C) -------------------------------------------------
   OPTIONAL and COLLAPSED at rest, so an ordinary customer's instant-price flow is visually unchanged
   and untouched. A trade customer opens the disclosure and gives name + email; the server matches the
   email to an approved account and applies the trade rate. Pure chrome — no price, no digit, no colour
   that duplicates a token. Its appearance is §75 build-then-review. */
.trade-id {
  margin-top: 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, .5);
}
.trade-id-summary {
  display: block;
  padding: 10px 12px;
  cursor: pointer;
  list-style: none;
}
.trade-id-summary::-webkit-details-marker { display: none; }
.trade-id-summary::after {
  content: "+";
  float: right;
  font-weight: 800;
  color: var(--muted);
}
.trade-id[open] .trade-id-summary::after { content: "\2212"; }
.trade-id-title { display: block; font-size: 12px; font-weight: 800; letter-spacing: .02em; color: var(--deep); }
.trade-id-hint { display: block; margin-top: 2px; font-size: 11px; line-height: 1.4; color: var(--muted); }
.trade-id-body { padding: 2px 12px 12px; }
.trade-id-note { margin: 2px 0 0; font-size: 11px; line-height: 1.4; color: var(--muted); }

/* ---- the choice row (the inspector's server-rendered option, Stage 2) -------------------------- */

.choice {
  position: relative;
  width: 100%;
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: 7px;
  align-items: center;
  padding: 11px 0 11px 25px;
  border: 0;
  border-bottom: 1px solid var(--line-soft);
  background: transparent;
  text-align: left;
}

.choice::before {
  content: "";
  position: absolute;
  left: 1px;
  top: 50%;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 1px solid rgba(12, 74, 111, .24);
  background: rgba(255, 255, 255, .72);
  transform: translateY(-50%);
}

.choice.active { background: linear-gradient(90deg, rgba(54, 188, 231, .10), transparent 82%); color: var(--blue); }
.choice.active::before { border-color: var(--blue); background: var(--blue); box-shadow: inset 0 0 0 4px rgba(255, 255, 255, .95); }
.choice b { display: block; font-size: 13px; color: var(--deep); }
.choice small { display: block; grid-column: 1; margin-top: 2px; font-size: 11px; line-height: 1.3; color: var(--muted); }

/* v0.17 — THE CATALOGUE-SHAPE THUMBNAIL (§75 shape thumbnails, rep-approved Studio-blue 40×32). It sits in
 * the `.choice` row's EXISTING auto column (`grid-template-columns: 1fr auto`), so the label keeps column 1
 * and the thumbnail is pinned to the row's right edge. The 40×32 slot is the whole of the new look; the
 * outline inside it is `.piece-outline` — the SAME `--blue` stroke and `--navy-rgb` wash the live drawing
 * already uses — so NO new colour or token is introduced. `preserveAspectRatio` (set on the node) fits the
 * shape's own viewBox into the slot, so a tall half-round and a wide racetrack both sit centred without any
 * per-shape sizing here. It spans BOTH rows of the choice grid so it stays centred when a `small` sub-line
 * is present. */
.choice-thumb {
  grid-column: 2;
  grid-row: 1 / -1;
  width: 40px;
  height: 32px;
  justify-self: end;
  align-self: center;
}

/* v0.21 — THE PER-ROW COLOUR SWATCH CHIP (§75 swatches, P47). It sits in the `.choice` row's EXISTING auto
 * column (`grid-template-columns: 1fr auto`), so the label keeps column 1 and the chip is pinned to the row's
 * right edge, exactly like `.choice-thumb`. The COLOUR is per-row DATA carried on the rect's `fill` attribute
 * (never a token here); this class is only the SLOT — a small rounded square with a hairline ring so a pale
 * swatch is still visible on the pale ground, reusing the SAME `--line-soft`/navy border the choice rows
 * already use, so NO new colour or token is introduced. It spans both grid rows to stay centred when a
 * `small` sub-line is present. 🔴 §75: the chip SIZE/PLACEMENT is afshin's appearance call — this is the
 * functional default flagged for his batch, exactly as the shape thumbnail was. */
.choice-swatch {
  grid-column: 2;
  grid-row: 1 / -1;
  width: 20px;
  height: 20px;
  justify-self: end;
  align-self: center;
  border-radius: 5px;
  box-shadow: inset 0 0 0 1px rgba(12, 74, 111, .24);
}

.mini-label {
  margin-bottom: 4px;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--faint);
}

/* ---- the interim questions (Stage 2 deletes this block with the markup it styles) --------------- */

.field { margin-bottom: 14px; }
.row { display: flex; flex-wrap: wrap; gap: 12px; }
.row .field { flex: 1 1 140px; }

label { display: block; margin-bottom: 4px; font-size: 12px; font-weight: 800; letter-spacing: .04em; color: var(--deep); }
.unit { font-weight: 500; color: var(--muted); }

input, select, textarea {
  width: 100%;
  padding: 8px 9px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: rgba(255, 255, 255, .92);
  color: var(--ink);
  font-size: 12px;
}

input:focus, select:focus, textarea:focus { outline: 2px solid var(--sky); outline-offset: 1px; }

/* The not-listed capture's project description. Same field chrome as every input; only the height and a
   vertical resize handle are its own, and its font follows the page rather than the browser default. */
textarea { min-height: 84px; resize: vertical; font-family: inherit; line-height: 1.5; }

/* ---- B1 · an unanswered required question, after the customer has tried to submit ---------------
   afshin's ruling, 2026-07-31: a red border plus a short sentence. No other visual change, and
   NOTHING AT REST — every rule below is gated on `[aria-invalid="true"]`, which `markUnanswered` sets
   only after a submit attempt and removes again the moment the box is filled.

   🔴 THE SELECTOR IS THE ARIA STATE ITSELF, NOT A CLASS THE PAGE ADDS BESIDE IT. `aria-required` and
   `aria-invalid` were already live and already correct — a screen-reader user has always been told
   which box was empty, and a sighted user never was. Styling the same attribute is what makes the two
   audiences see one truth: there is no arrangement of this page where the border and the assistive
   state disagree, because they ARE the same byte. A `.field-invalid` class set alongside would agree
   today and drift on the first edit to either (§113).

   🔴 NEITHER RULE CHANGES LAYOUT, deliberately. The input swaps a border COLOUR it already had; the
   choice group draws an inset ring rather than a real border, because a group that gained a 1px border
   would push its rows and make the whole inspector jump at the exact moment the customer is being told
   they made a mistake. */
input[aria-invalid="true"] { border-color: var(--danger); }
[role="radiogroup"][aria-invalid="true"] { border-radius: 10px; box-shadow: inset 0 0 0 1px var(--danger); }

/* ---- §75 shape-flow · EMPHASIZE THE SIZE INPUT (slice 8) ---------------------------------------
   afshin's points 4/8: once the shape is picked and the size step is reached, the DIMENSION boxes
   "light up" so the customer sees what to fill next. `form-renderer` puts `size-box` on every
   `control: 'dimension'` input (product-blind, the customer's own width/height), and because a step
   the customer has not reached is `[hidden]` (only the ACTIVE step renders), this emphasis is visible
   ONLY on the size step as it becomes reachable — the progressive-disclosure reveal afshin asked for.

   🔴 IT CHANGES NO LAYOUT, for the very reason the B1 note above states: the accent is a border COLOUR
   plus a soft box-shadow RING (the same no-jump technique the invalid radiogroup uses), never an added
   border width, so nothing shifts as the customer moves onto the step. It is the calm sky/blue of the
   unlocked rail dot, so the rail's "this step is live" cue and the box's "fill me" cue read as one
   language — and it is deliberately NOT the danger red.

   🔴 A RED BOX STAYS PURELY RED. The emphasis is gated `:not([aria-invalid="true"])`, so once a size
   box is flagged empty-required or geometry-bad (`markUnanswered`), the danger border wins alone and
   the two states never blend into an ambiguous colour. */
.size-box:not([aria-invalid="true"]) {
  border-color: var(--sky);
  box-shadow: 0 0 0 3px rgba(54, 188, 231, .13);
}

/* The sentence. It sits inside the `.field` block, under the control, so a question the customer's own
   answers have made irrelevant hides its prompt along with itself (`applyNotAsked` hides the block).
   It matches `.step-note`'s size and rhythm — the help line it sits beside — so the only thing that
   distinguishes it is the colour, which is the whole of what was approved. */
.field-required { margin-top: 4px; font-size: 12px; line-height: 1.4; color: var(--danger); }

/* ---- §162 reactive SIZE CEILING (afshin 2026-08-07) -------------------------------------------------
   The warning afshin ruled REACTIVE: the moment a customer types a dimension OVER the live admin ceiling,
   the box reddens AND blinks once, the form stops advancing, and an inline message states the consequence
   and links to the lead road — all reverting when they come back under. The RED is the SAME `aria-invalid`
   border the blank/geometry sets use (reused, no new colour); only the pieces below are new.

   🔴 THE MESSAGE matches `.field-required`'s size/rhythm — it sits in the same `.field` block, so a
   not-asked dimension takes it along — and carries the [N] value (`.field-ceiling-value`) the server
   rendered, with an inline LINK to the existing lead capture. */
.field-ceiling { margin-top: 4px; font-size: 12px; line-height: 1.4; color: var(--danger); }
.field-ceiling-value { font-weight: 600; }
.ceiling-lead-link {
  appearance: none; background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-decoration: underline; cursor: pointer;
}
.ceiling-lead-link:hover, .ceiling-lead-link:focus-visible { text-decoration: none; }

/* 🔴 BLINK ONCE, AND ONLY ON A CEILING BREACH. Keyed on the ceiling-ONLY `.over-ceiling` class (set by
   `markOverCeiling`), never on `aria-invalid` — so a blank-required or geometry-bad box reddens WITHOUT
   blinking. The box-shadow ring is free here (the sky emphasis ring is gated off once the box is
   aria-invalid, which an over-ceiling box always is). `animation-iteration-count: 1` + the class staying
   on while the box stays over = the blink fires ONCE and does not loop; removing the class on revert ends
   it, and a fresh over-value re-adds it and blinks again. */
@keyframes ceilingBlink {
  0%   { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
  30%  { box-shadow: 0 0 0 4px var(--danger); }
  100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
}
.size-box.over-ceiling { animation: ceilingBlink .45s ease 1; }

/* 🔴 ACCESSIBILITY RIDER (afshin's own, point (ب)): for a reader who finds motion distressing the REDNESS
   STAYS (the aria-invalid border is untouched) and only the BLINK is suppressed. CSS-only, so no
   `matchMedia` and no host global — the appearance is afshin's to approve and lives entirely here (§75). */
@media (prefers-reduced-motion: reduce) {
  .size-box.over-ceiling { animation: none; }
}

/* ---- §162 per-option LIABILITY DISCLOSURE PANELS (afshin 2026-10-05, restore-EXACT) ----------------
   The per-glass-type safety panel V1 shows beside the chooser and swaps as the customer picks (annealed /
   tempered / laminated). It sits inside the select's `.field` block, UNDER the control (so a not-asked
   chooser takes its panels along), hidden until `showOptionDisclosures` reveals the one matching the
   selected value — the `[hidden]` attribute does the hiding, as it does for the required/ceiling prompts.

   🔴 THE TEXT is the restore (verbatim from V1, byte-fidelity bound by a parity test); the CHROME is V2's
   own design system — tokens only, no V1 colour duplicated (RULE #1) — and, like the ceiling message's
   wording, the final visual treatment (and whether the note should carry a V1-style warn/safe colour
   split) is afshin's to approve (§75). It is a quiet, consistent callout until then. */
.disclosure-panel {
  margin-top: 6px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--sky);
  border-radius: var(--radius-sm);
  background: var(--paper);
}
.disclosure-title {
  font-size: 12px; font-weight: 700; line-height: 1.35; color: var(--navy);
  text-transform: uppercase; letter-spacing: .4px; margin-bottom: 2px;
}
.disclosure-desc { font-size: 12px; line-height: 1.4; color: var(--ink); margin-bottom: 4px; }
.disclosure-note { font-size: 11.5px; line-height: 1.4; color: var(--muted); }

/* ---- the results stage ------------------------------------------------------------------------- */

.result-stage { flex: 0 0 auto; display: grid; justify-items: center; gap: 18px; padding: 30px 20px 0; }

.result-card {
  width: min(680px, 100%);
  padding: 26px 28px;
  border: 1px solid rgba(var(--navy-rgb), .16);
  border-radius: var(--radius-lg);
  background: rgba(255, 255, 255, .94);
  box-shadow: 0 30px 80px rgba(24, 73, 100, .16);
  backdrop-filter: blur(22px);
  text-align: left;
}

.result-kicker {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--blue);
}

.result-kicker i { width: 8px; height: 8px; border-radius: 50%; background: var(--sky); box-shadow: 0 0 0 5px rgba(54, 188, 231, .10); }

.result-card .sub { margin: 0 0 18px; font-size: 13px; line-height: 1.5; color: var(--muted); }

/* the CLOSED-STATE banner (P13a) — the blessed result card read as a standing notice: a left accent in
   the brand blue, the kicker in the same token, and no bottom margin on its lone sentence. Reuses
   .result-card / .result-kicker / .sub so it is 100% consistent with the estimate card; appearance is
   reviewed post-deploy (§75). */
.result-card.site-closed { border-left: 3px solid var(--blue); }
.result-card.site-closed .sub { margin: 0; }

/* the CONTACT-mode banner (v0.22, DECISIONS 331/332) — a sticky top notice shown ON LOAD when the shop is
   in `contact` ("Contact for price") mode, ABOVE the live served form, so a customer knows at once no
   online price is coming. Token-only (no hex typed here — RULE #1/C-11): the brand-blue tint V1's banner
   reached for, done through the existing palette. It brings its OWN background AND text colour chosen
   together (V1's scar: a fallback colour that resolved dark-on-dark and made the ONE sentence a customer
   must read invisible). Appearance is reviewed post-deploy (§75); this is the V1 floor. */
.contact-banner {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--ice);
  border-bottom: 1px solid var(--line-strong);
  box-shadow: var(--shadow-soft);
}
.contact-banner-text {
  margin: 0;
  padding: 11px 20px;
  text-align: center;
  font-size: 13px;
  line-height: 1.5;
  color: var(--deep);
}
.contact-banner .contact-link {
  color: var(--blue);
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
}
.contact-banner .contact-link:hover { text-decoration: underline; }

.lines { width: 100%; border-collapse: collapse; }

.lines th, .lines td {
  padding: 11px 0;
  border-bottom: 1px solid rgba(12, 74, 111, .09);
  font-size: 14px;
  font-weight: 400;
  text-align: left;
  color: var(--ink);
}

.lines td { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--deep); }

.lines tfoot .grand th,
.lines tfoot .grand td {
  padding-top: 16px;
  border-bottom: 0;
  font-size: 16px;
  font-weight: 800;
  color: var(--deep);
}

.lines tfoot .grand td { font-size: 22px; letter-spacing: -.03em; }

/* 🔴 v0.13 — THE PRICE-FREE ESTIMATE (office-quote, "Quote by phone"). The plain spec-chip look of
   afshin's APPROVED package result card, expressed in THIS stylesheet's own tokens (--ice, --navy,
   --line) so it cannot drift from the palette: a pill per spec, and the existing `.ref-chip` for the
   estimate number. There is NO price and NO figure anywhere in this block — the wire carries none. */
.estimate-specs { margin: 2px 0; }
.spec-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; }
.spec-chip {
  padding: 7px 11px;
  font-size: 12px;
  color: var(--navy);
  background: var(--ice);
  border: 1px solid var(--line);
  border-radius: 999px;
}
.estimate-ref { display: flex; align-items: center; gap: 8px; margin: 16px 0 0; font-size: 12px; color: var(--muted); }
.estimate-ref-label { font-weight: 600; }
.estimate-ref .ref-chip { margin-top: 0; }

.result-actions { display: flex; gap: 10px; margin-top: 20px; flex-wrap: wrap; }

.btn-primary {
  flex: 1 1 200px;
  height: 46px;
  border: 0;
  border-radius: 13px;
  background: linear-gradient(135deg, var(--navy), var(--blue));
  color: #fff;
  font-size: 13px;
  font-weight: 800;
  box-shadow: 0 14px 30px rgba(var(--navy-rgb), .20);
  cursor: pointer;
}

.btn-primary:disabled { opacity: .55; cursor: progress; }

/* The QUIET half of the two-choice refusal (afshin, 2026-07-31): "edit your request" beside "submit
   for review". It is the same button geometry as .btn-primary so the pair sits level, and it states no
   colour of its own — every value below is an existing token. The two must be told apart at a glance
   without either looking disabled: one is the way back to the form, the other is the way to a human. */
.btn-quiet {
  flex: 1 1 200px;
  height: 46px;
  border: 1px solid var(--line);
  border-radius: 13px;
  background: transparent;
  color: var(--ink);
  font-size: 13px;
  font-weight: 800;
  cursor: pointer;
}

.result-fine { margin: 16px 0 0; font-size: 12px; line-height: 1.5; color: var(--faint); }

/* ==================================================================================================
 * 🔴 THE AI BAR (bottom) + THE SELF-EXPANDING PRESENTATION — v0.13 S2 (afshin's APPROVED render).
 * ==================================================================================================
 * The salesperson's composer sits at the BOTTOM as a bar; the SAME bar (`#aiBar`) SELF-EXPANDS into the
 * estimate PRESENTATION when a priced/price-free estimate arrives (`.grown` → fixed, over the studio).
 * Non-estimate result roads (missing-answer, lead, refusal, not-listed) show the presentation IN FLOW
 * below the studio (no `.grown`), so the inspector stays visible to fix a box.
 *
 * 🔴 VISIBILITY IS THE `hidden` ATTRIBUTE (ui-kit `show`), NEVER a `display` here — the one `!important`
 * in this file is `[hidden]{display:none}` and the invariant test forbids a second one, so `.grown` may
 * NOT toggle child display; app.js shows/hides `.ai-bar-live`/`.presentation` through the attribute. And
 * there is NO JS geometry (requestAnimationFrame/getBoundingClientRect are guard-refused host globals):
 * the grow is a CSS class swap, and the "soft motion" is a content fade/rise keyframe, no layout read. */
.ai-bar { position: relative; flex: 0 0 auto; z-index: 5; }

/* 🔴 PERSISTENT / ALWAYS-VISIBLE FOOTER — afshin's directive (auto-log [2026-09-08 19:40]:
 * «بار ای-آی تو پایین صفحه جایی باشه که همیشه دیده بشه مثل یک فوتر» — put the AI bar at the bottom,
 * somewhere it is always visible, like a footer). The RESTING composer is `position: sticky; bottom: 0`,
 * so it stays pinned to the viewport bottom while a tall form scrolls behind it and releases into flow
 * (above the fineprint) at the end of the scroll. Sticky stays in NORMAL FLOW, so the bar reserves its
 * own space and never overlaps the studio's controls or the live-preview canvas — no bottom-padding hack
 * and no JS geometry (requestAnimationFrame/getBoundingClientRect are guard-refused host globals).
 *
 * 🔴 RESTING ONLY, keyed off the presentation's own `hidden` state via `:has()` — never a new class, so
 * app.js keeps writing exactly `ai-bar` / `ai-bar grown` and no className test moves. When the bar
 * SELF-EXPANDS into a result the presentation is shown (loses `hidden`) and this rule stops matching: the
 * ESTIMATE road is `.ai-bar.grown` (its own `position: fixed`, below), and the DOCKED result road
 * (missing-answer / lead / refusal / not-listed) keeps the base `position: relative` so the presentation
 * still shows IN FLOW below the studio with the inspector visible to fix a box — both unchanged by this
 * rule. The z-index sits above the studio zones (head/inspector/rail) so the pinned footer is never
 * painted under them, and below the grown bar, the photo modal and the chat overlay. A browser without
 * `:has()` degrades to the pre-existing relative footer — safe, not broken.
 *
 * 🔴 PLACEMENT ONLY. It adds no wire, enables no AI and speaks no digit — the composer, its tenant-name
 * hook and its money-blind design are all unchanged. */
.ai-bar:has(> .presentation[hidden]) { position: sticky; bottom: 0; z-index: 40; }
.ai-bar-live { display: block; }

/* THE WATCH LINE + THE REVEAL — a centred row under the composer. The watch line NARRATES the existing
   B10 state in the page's OWN digit-free words; the reveal is an alternate trigger for the one quote road. */
.ai-bar-extra { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 2px 20px 14px; }
.ai-watch {
  margin: 0;
  max-width: 640px;
  text-align: center;
  font-size: 12px;
  line-height: 1.5;
  color: var(--blue);
  font-style: italic;
}
.ai-reveal {
  height: 40px;
  padding: 0 22px;
  border: 0;
  border-radius: var(--radius-sm);
  background: linear-gradient(135deg, var(--navy), var(--blue));
  color: #fff;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .02em;
  cursor: pointer;
  box-shadow: 0 12px 28px rgba(var(--navy-rgb), .20);
}
.ai-reveal:hover { transform: translateY(-1px); box-shadow: 0 16px 32px rgba(var(--navy-rgb), .26); }

/* THE PRESENTATION SURFACE — what the bar grows into. In flow (docked) by default; a full-height scroll
   surface when the bar is `.grown`. Its own paper backdrop so the studio does not read through. */
.presentation { width: 100%; }
.ai-bar.grown {
  position: fixed;
  inset: var(--topbar-h) 0 0 0;
  z-index: 60;
  overflow: auto;
  background:
    radial-gradient(70% 56% at 50% 12%, rgba(181, 229, 246, .34), transparent 62%),
    linear-gradient(180deg, #fbfdff, #f1f7fa);
}
.ai-bar.grown .presentation { animation: presIn .5s cubic-bezier(.2, .8, .2, 1) both; }
@keyframes presIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

/* THE PRESENTATION ACTIONS — Approve / Email, the customer's choice inside the estimate. Same geometry as
   the result buttons so they sit level; every colour is an existing token. `.btn-email` is the quiet half. */
.pres-actions { display: flex; gap: 12px; width: min(680px, 100%); margin: 18px auto 0; padding: 0 20px; flex-wrap: wrap; }
.btn-approve {
  flex: 1 1 200px;
  height: 46px;
  border: 0;
  border-radius: 13px;
  background: var(--ok);
  color: #fff;
  font-size: 13px;
  font-weight: 800;
  cursor: pointer;
  box-shadow: 0 14px 30px rgba(31, 158, 111, .22);
}
.btn-email {
  flex: 1 1 200px;
  height: 46px;
  border: 1px solid var(--line-strong);
  border-radius: 13px;
  background: var(--white);
  color: var(--navy);
  font-size: 13px;
  font-weight: 800;
  cursor: pointer;
}
.btn-approve:hover, .btn-email:hover { transform: translateY(-1px); }

/* THE EDIT VERB — the quiet third choice (v0.13 S3). Same geometry as Approve/Email so the three sit level;
   quiet fill, an existing token border. It takes the customer back to the form and composes no figure. */
.btn-edit {
  flex: 1 1 140px;
  height: 46px;
  border: 1px solid var(--line-strong);
  border-radius: 13px;
  background: transparent;
  color: var(--muted);
  font-size: 13px;
  font-weight: 800;
  cursor: pointer;
}
.btn-edit:hover { transform: translateY(-1px); color: var(--navy); }

/* THE APPROVE / RECEIVE PANELS — v0.13 S3, the c4 accept and the email-capture, reached from the choice.
   A centred card in the presentation, reusing the return page's APPROVED accept look (`.accept-check`,
   `.accept-fields`, `.keep-confirm`, `.terms-box`, `.ref-chip`). Every colour is an existing token; the
   §75 DEVIATION (these capture panels are not in the S2 self-expand render) is flagged for afshin's pass. */
.approve-panel, .receive-panel {
  width: min(560px, 100%);
  margin: 26px auto 40px;
  padding: 26px 26px 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: rgba(255, 255, 255, .94);
  box-shadow: 0 25px 60px rgba(24, 73, 100, .14);
}
.approve-terms { margin-bottom: 16px; }
.approve-terms-head { font-size: 13px; font-weight: 800; color: var(--deep); margin-bottom: 8px; }
.approve-form .accept-check, .receive-form .field:last-of-type { margin-bottom: 0; }
.approve-panel .btn-primary, .receive-panel .btn-primary { width: 100%; margin-top: 12px; }
/* THE MAIL GLYPH ON RECEIVE'S SAVED-CONFIRMATION — the same navy→blue fill the AI bar uses. Reused as-is. */
.exit-tick-mail { background: linear-gradient(135deg, var(--navy), var(--blue)); }

/* ---- the fine print ---------------------------------------------------------------------------- */

.fineprint {
  flex: 0 0 auto;
  margin-top: 30px;
  padding: 18px 20px 26px;
  border-top: 1px solid var(--line-soft);
  font-size: 12px;
  color: var(--faint);
  text-align: center;
}

/* ==================================================================================================
 * THE PRODUCT ROSTER — the landing page's service rail (products.html)
 * ==================================================================================================
 * afshin's 2026-08-01 ruling: the site's "All products" door shows the WHOLE roster — eight tiles —
 * because a page showing four of eight reads as an unfinished business. Four of them are real links
 * into the studio; four are marked and go nowhere, because the products behind them are not built.
 *
 * 🔴 THE CHROME IS THE APPROVED PACKAGE'S `.service` TILE, EXPRESSED IN THIS FILE'S TOKENS — the glass
 * gradient, the 13px radius, the inset white top edge, the dotted mask, the hairline highlight and the
 * hover lift are the landing "composer" rail's own, read off the approved package
 * (docs/qa/FRONTEND-DESIGN-MAP-2026-07-23.md §2-3). Nothing here declares a colour, a shadow or a
 * radius of its own: every value is a `--token` already in this file, so the roster cannot drift away
 * from the studio it leads into. There is no second stylesheet and there never will be (C-11).
 *
 * 🔴 THE COLUMN COUNT IS THE ONE THING THE PACKAGE DOES NOT ESTABLISH, AND IT IS FLAGGED RATHER THAN
 * PRESENTED AS APPROVED. The package's rail is `repeat(6, 1fr)` in ONE row, sized for six tiles at
 * `min(660px, 100%)`. afshin's ruling made it EIGHT, and eight in one row at that width gives ~76px of
 * tile — narrower than the package's own proportions and too narrow for its longest label. So the rail
 * is TWO ROWS OF FOUR here, and at narrow widths FOUR ROWS OF TWO. Both counts DIVIDE EIGHT, which is
 * the package's "no ragged row" rule kept mechanically rather than by eye — and it is asserted, in
 * `test/frontend/landing.test.ts`, so a fifth column can never leave a row half empty. If afshin wants
 * one row of eight instead, it is this one `grid-template-columns` value and nothing else.
 *
 * 🔴 GLYPHS — THE APPROVED LANDING NOW CARRIES THE PACKAGE'S PER-SHAPE ICONS (§75, the external
 * designer's package; build-then-review). Unlike the STUDIO rail (which still ships no rung icons — a
 * client-side step-id-to-glyph map there would be a per-product branch by another name), the LANDING
 * roster is a fixed, product-blind set of eight, so its icons are STATIC markup, not a client map: each
 * tile carries the designer's own SVG glyph as a sibling of its `<b>` label. The glyph is a stroked
 * outline with NO text and NO digit a walk can read (SVG geometry is exempt, like the composer's). Two
 * of the eight — Railing and Others — have no glyph in the package; those two are drawn plainly and are
 * FLAGGED for afshin's review. The icon sizing lives in the `.landing` block below. */

.roster {
  flex: 1 1 auto;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 20px;
  padding: 48px 20px 60px;
}

.service-rail {
  width: min(760px, 100%);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
}

/* The tile. ONE rule for both kinds — a live tile and a marked one must be the same object, or the
   roster stops looking like one row of one thing. What differs is stated below, and only below. */
.service {
  position: relative;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 5px;
  height: 84px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 13px;
  background: linear-gradient(145deg, rgba(255, 255, 255, .76), rgba(221, 242, 249, .42));
  box-shadow: inset 0 1px #fff, var(--shadow-soft);
  backdrop-filter: blur(12px);
  overflow: hidden;
  text-align: center;
  text-decoration: none;
  color: var(--ink);
}

/* the package's dotted veil and its hairline top highlight — both purely decorative, both `content: ""`
   because a `content:` carrying a digit is a figure the DOM walk cannot see (see the file header). */
.service::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(rgba(var(--navy-rgb), .08) .45px, transparent .5px);
  background-size: 3px 3px;
  opacity: .24;
  mask-image: linear-gradient(180deg, #000, transparent 75%);
  pointer-events: none;
}

.service::after {
  content: "";
  position: absolute;
  left: 15px;
  right: 15px;
  top: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .95), transparent);
}

.service b { font-size: 12px; font-weight: 650; line-height: 1.2; }

.service small {
  font-size: 8.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .16em;
  color: var(--faint);
}

/* 🔴 THE LIFT BELONGS TO THE LINK ALONE. A marked tile that lit up under the cursor would be promising
   a click it does not honour, which is the dead end this whole page exists to remove. */
a.service { cursor: pointer; transition: transform .25s, box-shadow .25s, border-color .25s; }

a.service:hover,
a.service:focus-visible {
  transform: translateY(-5px);
  border-color: var(--line-strong);
  box-shadow: inset 0 1px #fff, var(--shadow);
}

/* 🔴 MARKED, NOT DISABLED-LOOKING-BUT-CLICKABLE. `pointer-events` is deliberately NOT switched off: a
   click must LAND on this tile and demonstrably do nothing, which is what the browser proof measures.
   Inertness here is a property of the MARKUP — it is a `<span>`, it carries no href and the page ships
   no script — never of a style that a stylesheet edit could undo. */
.service-soon {
  cursor: default;
  opacity: .62;
  background: linear-gradient(145deg, rgba(255, 255, 255, .50), rgba(221, 242, 249, .22));
  color: var(--muted);
}

/* The AI line: shown, and inert for the same reason the studio ships no AI bar — its wire does not
   exist, and an inert INPUT is a dead end (index.html's own note, Stage 6). So it is a SENTENCE, with
   no box to type in and no button to press. */
.ai-line {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  padding: 11px 16px;
  border: 1px dashed var(--line);
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, .5);
  font-size: 12px;
  color: var(--muted);
}

.ai-line small {
  font-size: 8.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .16em;
  color: var(--faint);
}

@media (max-width: 820px) {
  /* still a divisor of eight — four full rows, never a ragged one */
  .service-rail { grid-template-columns: repeat(2, 1fr); }
}

/* ==================================================================================================
 * THE LANDING FRONT DOOR (products.html) — the external designer's approved look, in THIS file's tokens
 * ==================================================================================================
 * afshin's §75 approved landing (the external-designer package, build-then-review): a top bar with a
 * How-it-works / Service-area nav and a "Service Pro" button, a centred hero with the AI composer, the
 * eight-tile service rail, and a footer of four links. Everything below is SCOPED to `body.landing`
 * (the studio has no such class, so it is untouched) and every colour/radius/shadow is an EXISTING
 * --token — there is no second stylesheet and there never will be (C-11). No `content:` carries a digit.
 * The composer chrome, the AI glyph + its shards, the modal and the result-card are REUSED from the
 * studio blocks above; only the landing LAYOUT and the landing-only idle animations are added here. */

body.landing { overflow-x: hidden; }

/* the topbar's right-hand navigation (the shared .topbar is reused as-is) */
.top-links { display: flex; align-items: center; gap: 14px; }
.top-links a { font-size: 12px; text-decoration: none; color: var(--muted); transition: color .2s; }
.top-links a:hover { color: var(--navy); }
.pro-link {
  height: 34px;
  padding: 0 12px;
  display: flex;
  align-items: center;
  gap: 7px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, .62);
  box-shadow: 0 8px 28px rgba(29, 91, 126, .06);
  color: var(--navy);
}
.pro-link i { width: 7px; height: 7px; border-radius: 50%; background: var(--sky); box-shadow: 0 0 0 5px rgba(54, 188, 231, .10); }
@media (max-width: 700px) { .top-links > a:not(.pro-link) { display: none; } }

/* the brand LOGO image (the tenant logo hook). The studio still uses the CSS-drawn `.mark`; the landing
   uses a real image the book fills, so it needs its own frame. */
.brand-mark { flex: 0 0 auto; width: 30px; height: 30px; display: block; overflow: hidden; border-radius: 11px; box-shadow: 0 10px 22px rgba(20, 125, 168, .16); }
.brand-mark img { width: 100%; height: 100%; display: block; object-fit: cover; border-radius: 11px; }

/* the hero — centred between the topbar and the footer */
.hero {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  width: min(930px, 100%);
  margin: 0 auto;
  padding: 24px 20px 40px;
  text-align: center;
  animation: heroFold .5s ease both;
}
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-bottom: 8px;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--blue);
}
.hero h1 {
  margin: 0;
  font-weight: 600;
  font-size: clamp(28px, 3.8vw, 44px);
  line-height: 1.03;
  letter-spacing: -.04em;
  color: var(--deep);
  text-wrap: balance;
}
.hero h1 em { font-style: normal; color: var(--blue); }
.hero-sub { max-width: 560px; margin: 10px auto 16px; font-size: clamp(12px, 1.05vw, 14px); line-height: 1.4; color: var(--muted); }
@keyframes heroFold { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* the composer's glyph shimmer sweep (the designer's `.ai-glyph::after`), kept clipped inside the glyph */
.landing .ai-glyph::after {
  content: "";
  position: absolute;
  width: 10px;
  height: 60px;
  background: rgba(255, 255, 255, .8);
  transform: rotate(24deg);
  left: -16px;
  top: -14px;
  pointer-events: none;
  animation: shimmer 3.4s ease-in-out infinite;
}
@keyframes shimmer { 0%, 42% { left: -16px; opacity: 0; } 55% { opacity: .8; } 75%, 100% { left: 46px; opacity: 0; } }

/* the empty-submit nudge — driven by a class toggle in products.js (never a JS keyframe table). Only the
   landing composer ever gets `.shake`; the studio composer never does. */
.composer.shake { animation: composerShake .26s ease; }
@keyframes composerShake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-5px); }
  75% { transform: translateX(5px); }
}

/* the "Or start with a service" label, with its gentle idle motion */
.services-label {
  display: inline-block;
  margin: 16px 0 10px;
  font-size: 13px;
  font-weight: 650;
  letter-spacing: .01em;
  color: var(--blue);
  transform-origin: center;
  animation: serviceLabelMotion 3.2s ease-in-out infinite;
}
@keyframes serviceLabelMotion {
  0%, 100% { transform: translateY(0) scale(1); opacity: .88; }
  45% { transform: translateY(-3px) scale(1.035); opacity: 1; text-shadow: 0 5px 16px rgba(54, 188, 231, .22); }
  60% { transform: translateY(-1px) scale(1.01); opacity: .97; }
}

/* the rail on the landing — the shared .service tile plus the designer's per-shape icon and idle pulse */
.landing .service-rail { margin: 0 auto; }
.landing .service { opacity: .92; animation: cardPulse 5.8s ease-in-out infinite; }
.landing .service svg { width: 20px; height: 20px; color: var(--navy); opacity: .74; filter: drop-shadow(0 1px 0 #fff); }
.landing .service-soon { opacity: .62; }
.landing .service:nth-child(2) { animation-delay: .7s; }
.landing .service:nth-child(3) { animation-delay: 1.5s; }
.landing .service:nth-child(4) { animation-delay: 2.3s; }
.landing .service:nth-child(5) { animation-delay: 3.1s; }
.landing .service:nth-child(6) { animation-delay: 3.9s; }
.landing .service:nth-child(7) { animation-delay: 4.7s; }
.landing .service:nth-child(8) { animation-delay: 5.2s; }
@keyframes cardPulse {
  0%, 82%, 100% { transform: translateY(0); border-color: var(--line); box-shadow: inset 0 1px #fff, var(--shadow-soft); }
  88% { transform: translateY(-2px); border-color: var(--line-strong); box-shadow: inset 0 1px #fff, 0 18px 32px rgba(20, 92, 126, .11); }
}

/* the footer */
.bottom {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 14px;
  padding: 12px 20px 16px;
  font-size: 10px;
  color: var(--muted);
}
.bottom .copyright { font-size: 9px; color: var(--faint); }
.bottom-nav { display: flex; gap: 14px; flex-wrap: wrap; }
.bottom-nav a { color: var(--muted); text-decoration: none; transition: color .2s; }
.bottom-nav a:hover { color: var(--navy); }

/* 🔴 #4 — THE CONTACT / TRUST BLOCK: a full-width row above the ©/nav line, the shop's business hours and
   contact/trust details (server-injected into the empty data-tenant-* hooks). Uses only EXISTING tokens so
   it cannot drift from the rest of the footer. A line whose server value is empty (a raw static preview, no
   injection) collapses cleanly rather than showing a bare label. */
.footer-contact {
  flex: 0 0 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 7px 24px;
  padding-bottom: 12px;
  margin-bottom: 2px;
  border-bottom: 1px solid var(--line-soft);
}
.fc-item { display: flex; align-items: baseline; gap: 7px; font-size: 11px; line-height: 1.5; }
.fc-item:has(.fc-val:empty) { display: none; }
.fc-label {
  font-size: 9px; text-transform: uppercase; letter-spacing: .08em;
  font-weight: 700; color: var(--faint); white-space: nowrap;
}
.fc-val { color: var(--ink); font-weight: 500; }

/* the composer RESULT — the designer's card, COMING-SOON, no figure. Reuses .result-card / .result-kicker
   from the studio block above; only the fixed overlay position, the two-column layout, the close control
   and the "continue" link are added, and none carries a currency figure. */
.result {
  position: fixed;
  left: 50%;
  bottom: 24px;
  z-index: 40;
  width: min(760px, calc(100% - 34px));
  transform: translate(-50%, 14px);
  opacity: 0;
  pointer-events: none;
  transition: transform .4s cubic-bezier(.2, .8, .2, 1), opacity .4s;
}
.result.open { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
.landing .result-card { position: relative; width: 100%; display: grid; grid-template-columns: 1fr auto; gap: 24px; align-items: center; }
.result h3 { margin: 0 0 5px; font-size: 20px; letter-spacing: -.02em; color: var(--ink); }
.result p { margin: 0; font-size: 13px; line-height: 1.5; color: var(--muted); }
.result-side { display: flex; align-items: center; }
.result-close { position: absolute; right: 10px; top: 8px; width: 30px; height: 30px; border: 0; background: transparent; color: var(--faint); font-size: 20px; line-height: 1; cursor: pointer; }
.continue {
  height: 42px;
  padding: 0 16px;
  display: inline-flex;
  align-items: center;
  border: 0;
  border-radius: var(--radius-sm);
  background: linear-gradient(135deg, var(--navy), var(--blue));
  color: #fff;
  font-size: 12px;
  font-weight: 800;
  text-decoration: none;
  box-shadow: 0 12px 28px rgba(var(--navy-rgb), .20);
}
.continue:hover { transform: translateY(-1px); }

/* the photo preview inside the shared .drop, and the info-panel body */
.landing .drop { position: relative; overflow: hidden; }
.preview { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.doc-body { margin: 0; font-size: 13px; line-height: 1.55; color: var(--muted); }

/* the toast (new — the composer/photo coming-soon acknowledgements) */
.toast {
  position: fixed;
  left: 50%;
  bottom: 74px;
  z-index: 150;
  transform: translate(-50%, 20px);
  padding: 11px 16px;
  border-radius: 999px;
  background: var(--navy);
  color: #fff;
  font-size: 12px;
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s, transform .25s;
  box-shadow: 0 14px 35px rgba(var(--navy-rgb), .25);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

/* the landing result stacks on narrow screens */
@media (max-width: 700px) {
  .landing .result-card { grid-template-columns: 1fr; }
  .result-side { justify-content: stretch; }
  .continue { width: 100%; justify-content: center; }
}

/* ==================================================================================================
 * THE RETURN PAGE — a returning customer opens the estimate he already holds (estimate.html)
 * ==================================================================================================
 * The retrieve field, the opened priced estimate, and the terminal states. Every value below is a
 * `--token` already in this file — the aqua-glass palette, the shadows, the radii — so this surface
 * cannot drift away from the studio it belongs to. There is no second stylesheet and there never will
 * be (C-11), and nothing here is `!important` or an inline display (the hidden-attribute invariant).
 * The page STATES no amount, rate or date: every figure is painted at runtime from the wire, so no rule
 * here carries a currency figure or a money-named custom property. */

.return-main {
  flex: 1 1 auto;
  display: grid;
  justify-items: center;
  align-content: start;
  gap: 18px;
  padding: 30px 20px 0;
}

.return-screen { width: 100%; display: grid; justify-items: center; gap: 18px; }

/* ---- retrieve --------------------------------------------------------------------------------- */
.retrieve-screen { align-content: center; min-height: 56vh; }

.retrieve-card { width: min(430px, 100%); text-align: center; }

.retrieve-icon {
  width: 46px;
  height: 46px;
  border-radius: 13px;
  background: linear-gradient(145deg, var(--navy), var(--sky));
  display: grid;
  place-items: center;
  margin: 0 auto 14px;
  color: #fff;
}
.retrieve-icon svg { width: 22px; height: 22px; }

.retrieve-card h2 { margin: 0 0 6px; font-size: 20px; letter-spacing: -.02em; color: var(--deep); }
.retrieve-card p { margin: 0 0 18px; font-size: 13px; line-height: 1.55; color: var(--muted); }
.retrieve-card .field { text-align: left; }
.retrieve-card .field input { height: 44px; font-size: 14px; letter-spacing: .04em; text-transform: uppercase; }
.retrieve-card .btn-primary { width: 100%; }

.retrieve-error { margin: 0 0 12px; font-size: 12px; line-height: 1.4; color: var(--danger); text-align: left; }
.retrieve-help { margin: 14px 0 0; font-size: 12px; color: var(--muted); }

/* ---- the opened priced estimate --------------------------------------------------------------- */
.estimate-screen { width: min(880px, 100%); justify-items: stretch; }

.price-head {
  width: 100%;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  padding: 18px 24px;
}
.price-label { font-size: 10px; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; color: var(--faint); }
.ref-chip {
  display: inline-block;
  margin-top: 5px;
  padding: 3px 9px;
  font-size: 12px;
  font-weight: 700;
  color: var(--navy);
  background: var(--ice);
  border: 1px solid var(--line);
  border-radius: 8px;
}
.price-head-mid { text-align: center; min-width: 0; }
.price-head-mid strong { display: block; font-size: 16px; letter-spacing: -.01em; color: var(--deep); }
.price-head-mid .est-validity { display: block; margin-top: 4px; font-size: 12px; color: var(--muted); }
.price-head-total { text-align: right; }
.price-head-total span { display: block; font-size: 10px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; color: var(--faint); }
.price-head-total strong { display: block; margin-top: 3px; font-size: 26px; letter-spacing: -.03em; color: var(--deep); font-variant-numeric: tabular-nums; }

.return-grid {
  width: 100%;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  align-items: start;
  margin-top: 2px;
}

.panel { padding: 20px 22px; }
.panel-head { margin-bottom: 12px; }
.panel-head h3 { margin: 0; font-size: 14px; letter-spacing: -.01em; color: var(--deep); }

.line-rows { display: flex; flex-direction: column; }
.line-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 13px;
}
.line-label { color: var(--ink); }
.line-figure { color: var(--deep); font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.line-total {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--line-strong);
}
.line-total span { font-size: 13px; font-weight: 800; color: var(--deep); }
.line-total strong { font-size: 16px; font-weight: 800; color: var(--deep); font-variant-numeric: tabular-nums; }

.terms-box {
  max-height: 260px;
  overflow: auto;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: rgba(233, 247, 252, .5);
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
  white-space: pre-wrap;
}

.foot-note { width: 100%; margin: 18px 0 0; text-align: center; font-size: 12px; color: var(--faint); }
.foot-ref { font-weight: 700; }

/* ---- the accept tick + identity capture (c4) -------------------------------------------------- */
.accept-panel { width: 100%; margin-top: 16px; }
.accept-check {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  margin: 0 0 14px;
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.5;
  text-transform: none;
  color: var(--ink);
  cursor: pointer;
}
.accept-check input {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  margin-top: 1px;
  padding: 0;
  accent-color: var(--handle);
  cursor: pointer;
}
.accept-fields { display: flex; flex-wrap: wrap; gap: 12px; }
.accept-fields .field { flex: 1 1 180px; margin-bottom: 12px; }
.accept-fields .field input { height: 40px; }
.accept-panel .btn-primary { width: 100%; margin-top: 2px; }
.accept-error { margin: 0 0 12px; text-align: left; }
.accept-confirm { display: flex; gap: 12px; align-items: center; }
.accept-confirm .accept-tick {
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: rgba(244, 122, 36, .12);
  color: var(--handle);
}
.accept-confirm .accept-tick svg { width: 18px; height: 18px; }
.accept-confirm p { margin: 0; font-size: 13px; line-height: 1.5; font-weight: 600; color: var(--deep); }

/* ---- what happens next — the fallback call-to-book CTA (V1-parity restoration, §162) ----------
   The next-step the customer takes after seeing his estimate: a tel: link to confirm & book, plus the
   "Estimate <ref> when you call us on <phone>" line. No money/figure here — it reuses the house primary
   look (the same --navy/--blue gradient and #fff idiom as .btn-primary) so it stays visually consistent
   (RULE 1); every colour below is an existing token. The disabled state (no tenant phone on the book) is
   non-clickable via pointer-events, mirroring the lock-mark convention used elsewhere on these pages. */
.next-step-panel { width: 100%; margin-top: 16px; }
.next-step-cta {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 13px 18px;
  border-radius: 13px;
  background: linear-gradient(135deg, var(--navy), var(--blue));
  color: #fff;
  text-decoration: none;
  text-align: center;
  font-size: 14px;
  font-weight: 800;
  box-shadow: 0 14px 30px rgba(var(--navy-rgb), .20);
}
.next-step-cta small { font-size: 12px; font-weight: 500; opacity: .85; }
.next-step-cta:hover { filter: brightness(1.04); }
.next-step-cta.is-disabled { pointer-events: none; opacity: .55; }
.next-step-note { margin: 12px 0 0; text-align: center; font-size: 12.5px; line-height: 1.5; color: var(--muted); }
.next-step-ref, .next-step-phone { font-weight: 700; color: var(--deep); white-space: nowrap; }

/* ---- the KEEP surfaces on the priced card (return-path ENTRY) — mirrors the accept-confirm look
   for visual consistency (RULE 1); the tick, chip and text tokens are the same as the accept tick. */
.keep-actions { margin-top: 18px; }
.keep-confirm { display: flex; gap: 12px; align-items: center; margin-top: 18px; }
.keep-confirm .accept-tick {
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: rgba(244, 122, 36, .12);
  color: var(--handle);
}
.keep-confirm .accept-tick svg { width: 18px; height: 18px; }
.keep-confirm-text { min-width: 0; }
.keep-confirm p { margin: 0; font-size: 13px; line-height: 1.5; font-weight: 600; color: var(--deep); }
.keep-confirm .keep-sub { margin-top: 3px; font-weight: 500; color: var(--muted); }
.keep-error { margin: 14px 0 0; font-size: 12px; line-height: 1.4; font-weight: 600; color: var(--danger); }

/* ---- the terminal states ---------------------------------------------------------------------- */
.state-screen { align-content: center; min-height: 56vh; }
.state-card { width: min(440px, 100%); text-align: center; }
.state-icon {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  margin: 0 auto 14px;
  background: rgba(244, 122, 36, .12);
  color: var(--handle);
}
.state-icon svg { width: 24px; height: 24px; }
.state-card h2 { margin: 0 0 8px; font-size: 19px; letter-spacing: -.02em; color: var(--deep); }
.state-card p { margin: 0 0 20px; font-size: 13px; line-height: 1.6; color: var(--muted); }
.state-actions { display: flex; justify-content: center; }
.state-actions .btn-primary { flex: 0 1 240px; }

@media (max-width: 720px) {
  .price-head { grid-template-columns: 1fr auto; row-gap: 8px; }
  .price-head-mid { grid-column: 1 / 3; order: 3; text-align: left; }
  .return-grid { grid-template-columns: 1fr; }
}

/* ==================================================================================================
 * THE TRADE-ACCOUNT PAGE — pro.html (Pro-A). SCOPED to `body.pro`. It reuses the shared tokens, the
 * masthead (.topbar/.brand), the form controls (.field/label/input/.btn-primary), the fineprint footer
 * and the .accept-tick — this block is only the page's own two-column intro/application layout, in the
 * Studio design language. It paints no figure and names no price (a trade application has none).
 * ================================================================================================== */
body.pro { overflow-x: hidden; }
.pro-shell { }
.pro-main {
  flex: 1 0 auto;
  width: 100%;
  max-width: 1120px;
  margin: 0 auto;
  padding: 30px 20px 48px;
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: 34px;
  align-items: start;
}
.pro-intro { animation: heroFold .5s ease both; }
.pro-intro h1 {
  margin: 6px 0 12px;
  font-size: 30px;
  line-height: 1.14;
  letter-spacing: -.02em;
  color: var(--deep);
}
.pro-lead { margin: 0 0 22px; font-size: 16px; line-height: 1.6; color: var(--muted); max-width: 46ch; }
.pro-perks { list-style: none; margin: 0 0 22px; padding: 0; display: grid; gap: 14px; }
.pro-perks li { display: flex; gap: 13px; align-items: flex-start; }
.pro-perk-ic {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  background: var(--ice);
  color: var(--blue);
  box-shadow: inset 0 0 0 1px rgba(var(--navy-rgb), .10);
}
.pro-perk-ic svg { width: 20px; height: 20px; }
.pro-perk-copy { font-size: 14px; line-height: 1.55; color: var(--ink); }
.pro-perk-copy b { display: block; font-size: 14.5px; color: var(--deep); margin-bottom: 2px; }
.pro-who { margin: 0; font-size: 12.5px; line-height: 1.6; color: var(--faint); max-width: 52ch; }

.pro-card {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 24px 24px 22px;
  box-shadow: 0 20px 44px rgba(var(--navy-rgb), .10);
  position: sticky;
  top: 22px;
}
.pro-card-head { margin-bottom: 16px; }
.pro-card-head h2 { margin: 0 0 4px; font-size: 19px; letter-spacing: -.01em; color: var(--deep); }
.pro-card-head .sub { margin: 0; font-size: 13px; line-height: 1.5; color: var(--muted); }
.pro-card label em { text-transform: none; font-weight: 500; color: var(--faint); letter-spacing: 0; }
.pro-action { margin-top: 18px; }
.pro-action .btn-primary { width: 100%; }
.pro-msg { margin: 12px 0 0; color: var(--danger); }
.pro-fine { margin: 12px 0 0; font-size: 12px; line-height: 1.5; color: var(--faint); }

.pro-done { display: flex; gap: 14px; align-items: flex-start; }
.pro-done .accept-tick { flex: 0 0 auto; }
.pro-done-text h2 { margin: 2px 0 6px; font-size: 20px; letter-spacing: -.01em; color: var(--deep); }
.pro-done-text .sub { margin: 0; font-size: 14px; line-height: 1.6; color: var(--muted); }
.pro-done-text a { color: var(--blue); font-weight: 700; text-decoration: none; }
.pro-done-text a:hover { text-decoration: underline; }

@media (max-width: 860px) {
  .pro-main { grid-template-columns: 1fr; gap: 26px; padding-top: 22px; }
  .pro-card { position: static; }
  .pro-intro h1 { font-size: 25px; }
}
