/* 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(70% 56% at 50% 30%, rgba(181, 229, 246, .40), transparent 63%),
    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: 10px;
  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.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); }
.tool.locked { opacity: .38; filter: saturate(.45); }

.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: 8px;
  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: 10.5px; 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: 10px; 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: 12px; color: var(--deep); }
.choice small { display: block; grid-column: 1; margin-top: 2px; font-size: 9px; line-height: 1.3; color: var(--muted); }

.mini-label {
  margin-bottom: 4px;
  font-size: 8px;
  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: 10px; 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); }

/* 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: 10px; line-height: 1.4; color: var(--danger); }

/* ---- 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); }

.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: 11px; 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; }
.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: 11px;
  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.
 *
 * 🔴 NO GLYPHS, AND THE ABSENCE IS THE EXISTING DECISION RATHER THAN A NEW ONE. The package draws an
 * icon per tile; `index.html` already records afshin's call (§75) that the studio rail ships NO rung
 * icons, because a client-side id-to-glyph map is a per-product branch by another name. Two of these
 * eight products have no glyph in the package at all, so drawing them would be inventing appearance
 * nobody approved. The tiles carry their words and nothing else. */

.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 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: 11px; 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: 11.5px;
  line-height: 1.6;
  color: var(--muted);
  white-space: pre-wrap;
}

.foot-note { width: 100%; margin: 18px 0 0; text-align: center; font-size: 11.5px; 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); }

/* ---- 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; }
}
