/* ============================================================================
   Your Dashboard — Midnight Marble.
   ----------------------------------------------------------------------------
   BRAND.md's approved direction: a near-black architectural field, cool-white
   mineral text, a glacial-teal action signal, gold reserved for warnings, and
   enormous type. Ported from the two surfaces that already speak it — the
   marketing site (flagship-site/public/styles/midnight-marble.css) and the
   staff console (crm/staff-v2/src/styles.css), whose token NAMES this file
   mirrors exactly so the two dark app surfaces can converge later without a
   redesign.

   Every hex lives in the :root block below and nowhere else, because
   test/cockpit-contrast.test.ts parses that block and gates each pair against
   WCAG 2.2 AA. A hex written inline is a hex nothing checks. Retune colour by
   editing a token and re-running `npm test`.

   Accessibility floor (product spec §10, L391–397): 48px tap targets, visible
   focus, AA contrast, colour never the only signal, reduced motion honoured.
   Deliberately NOT here: WebGL, scrolljacking, carousels, custom cursors,
   autoplay, or motion under body copy — §10 bans all of them, and the surface
   is meant to feel premium by being calm, not busy.
   ============================================================================ */

/* ── Type ────────────────────────────────────────────────────────────────────
   Two families, self-hosted, per spec §10. Both files are variable, so one
   download covers every weight the shell uses. Latin + latin-ext only: a
   business name with an accent in it must render, but nothing here needs
   Cyrillic or Greek and shipping them would triple the bytes. */

@font-face {
  font-family: "Sora";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("/fonts/sora-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Sora";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("/fonts/sora-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/fonts/inter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/fonts/inter-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ── Tokens ─────────────────────────────────────────────────────────────────
   Contrast-gated hexes first (the test reads these by name), then the
   ungated supporting values: hairlines, washes and glows are all translucent
   and carry no text, so no ratio applies to them. */

:root {
  /* Surfaces — the midnight field, darkest at the page and lifting toward the
     things you can act on. */
  --surface-page: #0a0e14;
  --surface-inset: #0c1119;
  --surface-navy: #0d131c;
  --surface-card: #10161f;
  --surface-raised: #161e2a;
  --surface-action: #0d6a63;

  /* Signals. Teal is the ONE accent for the primary action on a view (§10);
     gold is reserved for the staff-view banner and warnings, so its appearance
     always means "read this". */
  --accent-teal: #4fd1c0;
  --accent-gold: #f3a81b;
  --danger-action: #a62639;

  /* Text. */
  --text-primary: #e9eef4;
  --text-strong: #ffffff;
  --text-muted: #9ab0bf;
  --success-text: #4fc47f;
  --warn-text: #e6b04b;
  --danger-text: #ff8d9d;

  /* Status discs — non-text, gated at 3:1 against the card they sit on. */
  --dot-green: #1e8549;
  --dot-amber: #976b00;
  --dot-red: #ff6b6b;

  /* Focus. Two colours so the ring survives both the page and a filled
     button: a white inner outline with a glacial glow ring around it. */
  --focus-ring-light: #ffffff;
  --focus-ring-dark: #53e0cd;

  /* Supporting values (translucent; no contrast gate applies). */
  --line-hairline: rgba(154, 176, 191, 0.16);
  --line-strong: rgba(154, 176, 191, 0.34);
  --line-teal: rgba(79, 209, 192, 0.42);
  --line-gold: rgba(243, 168, 27, 0.42);
  /* Near-opaque, not glassy: the studio's panels sit over a live web page that
     may be white and busy, and body copy has to win against whatever is behind
     it. The blur is atmosphere; the alpha is what keeps the words readable. */
  --glass-card: rgba(16, 22, 31, 0.94);
  --wash-teal: rgba(79, 209, 192, 0.09);
  --wash-gold: rgba(243, 168, 27, 0.09);
  --glow-teal: rgba(79, 209, 192, 0.22);
  --scrim: rgba(4, 7, 11, 0.78);
  --glass-navy: rgba(13, 19, 28, 0.86);

  /* Your AI Brain — the completeness ring, the knowledge map, and the learned
     layer. Every one of these is translucent and carries no text, so no
     contrast pair applies to any of them: the surface deliberately introduces
     no new gated colour, reusing the text tones and surfaces already measured
     above. The teal glow is --surface-action at low alpha, so the map's core
     glows in the same hue as the one primary action. */
  /* The completeness ring's circumference (2πr, r=52). lib/brain-map.js sets it
     per element so the keyframe can animate from "empty" without knowing the
     value — declared here as well so the property is never undefined, which is
     both what the token gate requires and what makes the reveal still work if
     the inline style is ever dropped. */
  --ring-c: 326.73;
  --ring-track: rgba(154, 176, 191, 0.18);
  --map-grid: rgba(154, 176, 191, 0.1);
  --map-node-empty: rgba(154, 176, 191, 0.28);
  --map-link: rgba(79, 209, 192, 0.28);
  --glow-teal-deep: rgba(13, 106, 99, 0.34);
  --wash-learned: rgba(154, 176, 191, 0.06);

  /* Geometry. Near-square corners are the Midnight Marble signature — the
     marketing site runs 2px/4px and the softness of a 10px radius is most of
     what made this shell read as a generic template. */
  --radius-sm: 2px;
  --radius-md: 3px;
  --radius-lg: 5px;
  --radius-pill: 999px;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;

  --tap-min: 48px;
  --measure: 72rem;

  --font-display: "Sora", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-body: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;

  /* Inside §10's 150–220ms band. One number so nothing drifts out of it. */
  --motion: 180ms;

  color-scheme: dark;
}

* {
  box-sizing: border-box;
}

html {
  min-width: 320px;
}

/* The midnight field: two ambient glows at the top of the page plus a faint
   mineral dot-grid. Plain background layers only — a fixed masked overlay
   wedges Chrome's compositor, which is the lesson staff-v2 already paid for. */
body {
  min-width: 320px;
  min-height: 100svh;
  margin: 0;
  background-color: var(--surface-page);
  background-image:
    radial-gradient(ellipse 58% 44% at 16% -4%, rgba(29, 41, 51, 0.55), transparent 72%),
    radial-gradient(ellipse 46% 38% at 86% -2%, rgba(13, 106, 99, 0.16), transparent 74%),
    radial-gradient(rgba(174, 185, 184, 0.07) 0.6px, transparent 0.6px);
  background-size: 100% 100%, 100% 100%, 44px 44px;
  background-repeat: no-repeat, no-repeat, repeat;
  background-attachment: fixed, fixed, fixed;
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.6;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

::selection {
  background: rgba(79, 209, 192, 0.32);
}

a,
button {
  -webkit-tap-highlight-color: transparent;
}

/* Two-colour focus indicator: white inner outline + glacial glow ring. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--focus-ring-light);
  outline-offset: 2px;
  box-shadow: 0 0 0 6px var(--focus-ring-dark);
}

.skip-link {
  position: fixed;
  z-index: 60;
  top: var(--space-3);
  left: var(--space-3);
  padding: var(--space-3) var(--space-4);
  color: var(--text-strong);
  background: var(--surface-action);
  border: 1px solid var(--line-teal);
  border-radius: var(--radius-md);
  transform: translateY(-200%);
}

.skip-link:focus {
  transform: translateY(0);
}

/* ── Type scale ──────────────────────────────────────────────────────────── */

h1,
h2,
h3 {
  font-family: var(--font-display);
  color: var(--text-primary);
  text-wrap: balance;
}

h1 {
  margin: 0 0 var(--space-4);
  font-size: clamp(1.9rem, 4.4vw, 2.75rem);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.05;
}

/* Home's greeting. Enormous is the brief — this is the one place the type is
   allowed to dominate, because it is also the only place the screen is
   addressing the person by name. */
.hello {
  margin: 0 0 var(--space-8);
  font-size: clamp(2rem, 6vw, 3.75rem);
  letter-spacing: -0.05em;
  line-height: 0.98;
}

h2 {
  margin: 0 0 var(--space-4);
  font-size: clamp(1.25rem, 2.4vw, 1.5rem);
  font-weight: 750;
  letter-spacing: -0.02em;
  line-height: 1.15;
}

h3 {
  margin: 0;
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.015em;
  line-height: 1.25;
}

p {
  margin: 0 0 var(--space-4);
  max-width: 62ch;
}

.muted {
  color: var(--text-muted);
}

/* Uppercase display label. Used for section heads and rail groups — the
   wide tracking is what makes a small label read as architecture rather than
   as shrunken body copy. */
.eyebrow {
  margin: 0 0 var(--space-2);
  font-family: var(--font-display);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Anything numeric lines up column-wise across rows (§10: tabular numerals). */
.tnum {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.icon {
  width: 1.25rem;
  height: 1.25rem;
  flex: 0 0 auto;
}

/* ── Chrome ─────────────────────────────────────────────────────────────────
   A sticky glass header. Its job is not decoration: it is the only thing on
   screen that says whose dashboard this is and offers the way out, on every
   view, without a screen swap being able to remove it. */

.chrome {
  position: sticky;
  top: 0;
  z-index: 20;
  background:
    linear-gradient(115deg, rgba(79, 209, 192, 0.055), transparent 46%),
    var(--glass-navy);
  backdrop-filter: blur(12px) saturate(115%);
  -webkit-backdrop-filter: blur(12px) saturate(115%);
  border-bottom: 1px solid var(--line-hairline);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .chrome {
    background:
      linear-gradient(115deg, rgba(79, 209, 192, 0.055), transparent 46%),
      var(--surface-navy);
  }
}

/* The marble vein: a glacial signal line that fades out across the header. */
.chrome::after {
  content: "";
  position: absolute;
  inset: auto 0 -1px 0;
  height: 1px;
  background: linear-gradient(90deg, var(--accent-teal), rgba(79, 209, 192, 0) 62%);
  opacity: 0.7;
  pointer-events: none;
}

.chrome__row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  width: min(100% - 2.5rem, var(--measure));
  margin-inline: auto;
  min-height: 4.5rem;
  padding-block: var(--space-3);
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  color: var(--text-primary);
  text-decoration: none;
}

/* It is a link on every screen, so it gets a hit area like one. */
a.brand {
  min-height: var(--tap-min);
}

a.brand:hover .brand__name {
  color: var(--accent-teal);
}

.brand__mark {
  width: 1.75rem;
  height: 1.18rem;
  flex: 0 0 auto;
}

.brand__name {
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

/* Which business this is. Beside the wordmark on a phone, pushed to the middle
   on a wide screen — it is the answer to "am I in the right account?". */
.chrome__client {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  max-width: none;
  min-width: 0;
  padding-left: var(--space-4);
  border-left: 1px solid var(--line-hairline);
  color: var(--text-muted);
  font-size: 0.9rem;
}

.chrome__client-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-primary);
  font-weight: 600;
}

.chrome__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
}

/* On a phone the three parts do not fit on one line, and left to wrap on their
   own they produced a three-row header taller than the content under it. So the
   business name takes a row of its own beneath the brand and the way out, which
   is also the order they matter in when the screen is 375px wide. */
@media (max-width: 34rem) {
  .chrome__row {
    row-gap: var(--space-2);
    min-height: 0;
  }

  .chrome__client {
    order: 3;
    flex: 1 0 100%;
    padding-left: 0;
    border-left: 0;
  }

  .chrome__actions button {
    padding: 0 var(--space-3);
    font-size: 0.78rem;
  }
}

/* ── Layout ─────────────────────────────────────────────────────────────── */

main {
  display: block;
  width: min(100% - 2.5rem, var(--measure));
  margin-inline: auto;
  padding-block: var(--space-10) var(--space-12);
}

.section {
  margin-bottom: var(--space-10);
}

.section__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.section__head h2 {
  margin: 0;
}

.footer {
  width: min(100% - 2.5rem, var(--measure));
  margin-inline: auto;
  padding-block: var(--space-6) var(--space-10);
  border-top: 1px solid var(--line-hairline);
  color: var(--text-muted);
  font-size: 0.875rem;
}

/* One support path is always visible (§10). */
.footer__support {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  color: var(--accent-teal);
}

/* ── Controls ───────────────────────────────────────────────────────────── */

/* `.primary` is included in the BASE rule, not only in the accent rule below,
   because it lands on `<a>` as often as on `<button>` (the service tiles open a
   hash route). Without it an anchor would take the accent fill and none of the
   geometry — including the 48px tap target.

   `label.primary` is here for the same reason and one more: the photo library's
   "Add photos" control is a real `<label for>` over a visually hidden file
   input, because that native pairing is what keeps it keyboard-reachable and
   correctly announced. It has to look and measure like every other button. */
button,
.btn,
a.primary,
label.primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: 0 var(--space-6);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-primary);
  font-family: var(--font-display);
  font-size: 0.875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--motion) ease,
    border-color var(--motion) ease,
    color var(--motion) ease,
    box-shadow var(--motion) ease;
}

button:hover,
.btn:hover,
a.primary:hover {
  border-color: var(--accent-teal);
  background: var(--wash-teal);
}

/* The one accent action on a view. */
button.primary,
.primary {
  border-color: var(--line-teal);
  background: var(--surface-action);
  color: var(--text-strong);
}

button.primary:hover,
.primary:hover {
  background: var(--surface-action);
  border-color: var(--accent-teal);
  box-shadow: 0 0 0 1px var(--glow-teal), 0 12px 30px rgba(13, 106, 99, 0.28);
}

.ghost {
  border-color: var(--line-hairline);
  background: transparent;
  color: var(--text-muted);
}

.ghost:hover {
  color: var(--text-primary);
}

.danger {
  border-color: var(--danger-action);
  color: var(--danger-text);
}

.danger:hover {
  background: rgba(166, 38, 57, 0.18);
  border-color: var(--danger-text);
}

button[disabled],
.btn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

button[disabled]:hover {
  background: transparent;
  border-color: var(--line-strong);
  box-shadow: none;
}

label {
  color: var(--text-muted);
  font-size: 0.9rem;
}

input,
textarea {
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  color: var(--text-primary);
  font-family: var(--font-body);
  /* 16px minimum: anything smaller makes iOS Safari zoom the whole page on
     focus, which throws the client out of the layout they were editing. */
  font-size: 1rem;
  transition: border-color var(--motion) ease, background-color var(--motion) ease;
}

input:hover,
textarea:hover {
  border-color: var(--line-strong);
}

textarea {
  resize: vertical;
  line-height: 1.55;
}

::placeholder {
  color: var(--text-muted);
  opacity: 0.75;
}

/* Reserve the line so an arriving message never shifts the layout under a
   thumb that is already moving toward a button. */
.status {
  min-height: 1.6em;
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}

/* Status carries a WORD; tone only reinforces it — colour is never the sole
   signal (WCAG 2.2 AA; product spec L391). */
.status[data-tone="ok"] { color: var(--success-text); }
.status[data-tone="warn"] { color: var(--warn-text); }
.status[data-tone="error"] { color: var(--danger-text); }

/* ── Status disc ────────────────────────────────────────────────────────────
   Ring + core, so the states differ in SHAPE as well as hue and survive
   greyscale, colour blindness, and a sunlit phone. */

.dot {
  width: 0.85rem;
  height: 0.85rem;
  flex: 0 0 auto;
}

.dot__ring {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  opacity: 0.45;
}

.dot__core {
  fill: currentColor;
}

.dot--live { color: var(--success-text); }
.dot--setup { color: var(--warn-text); }
.dot--off { color: var(--text-muted); }
.dot--alert { color: var(--danger-text); }

/* The live dot breathes, once every 2.4s — slow enough to read as "this is
   current" rather than as something demanding attention. Nothing under body
   copy moves (§10). */
.dot--live .dot__ring {
  animation: breathe 2.4s ease-in-out infinite;
  transform-origin: center;
}

@keyframes breathe {
  0%, 100% { opacity: 0.45; transform: scale(1); }
  50% { opacity: 0.9; transform: scale(1.12); }
}

/* ── The status core ────────────────────────────────────────────────────────
   Health first (product spec L216–230) — the order is not cosmetic; it is
   what lets an owner answer "is everything working?" in five seconds on a
   phone. So the answer is the largest thing on the screen after their name,
   and the numbers under it are the evidence. */

.core {
  position: relative;
  overflow: hidden;
  margin-bottom: var(--space-8);
  padding: var(--space-8);
  background:
    radial-gradient(ellipse 60% 120% at 0% 0%, var(--wash-teal), transparent 62%),
    var(--surface-card);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-lg);
}

/* A hairline of signal along the top edge, brightest where the glow is. */
.core::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg, var(--accent-teal), rgba(79, 209, 192, 0) 55%);
  opacity: 0.75;
}

.core__verdict {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0;
  max-width: none;
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 3.2vw, 1.9rem);
  font-weight: 750;
  letter-spacing: -0.03em;
  line-height: 1.2;
}

.core__verdict .dot {
  width: 1.1rem;
  height: 1.1rem;
}

/* Three fixed columns rather than auto-fit: the values are one or two digits,
   so they fit side by side even at 320px, and letting them stack turned the
   health answer into a full screen of scrolling on a phone — which is the one
   thing this block exists to prevent. */
.core__metrics {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-4);
  margin-top: var(--space-6);
  padding-top: var(--space-6);
  border-top: 1px solid var(--line-hairline);
}

@media (min-width: 40rem) {
  .core__metrics {
    gap: var(--space-6);
    margin-top: var(--space-8);
  }
}

.metric__value {
  display: block;
  font-family: var(--font-display);
  font-size: 1.65rem;
  font-weight: 750;
  letter-spacing: -0.02em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.metric__label {
  display: block;
  margin-top: var(--space-1);
  color: var(--text-muted);
  font-size: 0.82rem;
  letter-spacing: 0.02em;
}

/* ── Service tiles ──────────────────────────────────────────────────────── */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--space-4);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* The catalog of services not switched on yet. Denser than the grid above it
   on purpose: the difference in tile size is the hierarchy, and without it the
   ten things a client does NOT have would out-weigh the ones they do. */
.grid--catalog {
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--space-3);
}

.tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-6);
  background: var(--surface-card);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-lg);
  transition: border-color var(--motion) ease, background-color var(--motion) ease;
}

/* The signal edge. Dim at rest, glacial on hover — the tile answers before
   the pointer lands on the button. */
.tile::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg, var(--accent-teal), rgba(79, 209, 192, 0) 60%);
  opacity: 0.28;
  transition: opacity var(--motion) ease;
}

.tile:hover {
  border-color: var(--line-strong);
  background: var(--surface-raised);
}

.tile:hover::before {
  opacity: 0.85;
}

/* Services that are not switched on yet. They read as capacity, not as
   controls: dimmer, no accent edge, and deliberately no button — an
   unentitled service must never mimic a panel it cannot drive (spec L255). */
.tile--slot {
  background: transparent;
  border-style: dashed;
  border-color: var(--line-hairline);
}

.tile--slot::before {
  display: none;
}

.tile--slot:hover {
  background: var(--surface-inset);
  border-color: var(--line-strong);
}

.tile--slot {
  padding: var(--space-4);
  gap: var(--space-2);
}

.tile--slot .tile__title,
.tile--slot .tile__icon {
  color: var(--text-muted);
}

.tile--slot .tile__title {
  font-size: 0.95rem;
}

.tile--slot .tile__summary {
  font-size: 0.85rem;
}

.tile__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.tile__icon {
  color: var(--accent-teal);
}

.tile__title {
  flex: 1 1 auto;
  min-width: 0;
}

.tile__summary {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.925rem;
  line-height: 1.5;
}

.tile__foot {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-top: auto;
  padding-top: var(--space-2);
}

/* State chip: disc + word, never the disc alone. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3) var(--space-1) var(--space-2);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-pill);
  background: var(--surface-inset);
  color: var(--text-muted);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.tile__foot .primary {
  margin-left: auto;
}

/* ── Gate screens (sign-in, chooser, paused, unavailable) ───────────────────
   The first thing anyone ever sees of this product, and until now it was an
   h1 and a bare input on a black field. */

/* Signed out there is no chrome and no content below, so the card is the whole
   screen — it should sit in the middle of it rather than clinging to the top.
   Browsers without :has() simply keep the top-aligned card. */
main:has(> .gate) {
  display: grid;
  place-items: center;
  min-height: calc(100svh - var(--space-12) - var(--space-10));
}

.gate {
  width: min(100%, 27rem);
  margin: var(--space-10) auto;
  padding: var(--space-10) var(--space-8);
  background:
    radial-gradient(ellipse 80% 100% at 50% 0%, var(--wash-teal), transparent 60%),
    var(--surface-card);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-lg);
}

.gate__brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-8);
}

.gate__brand .brand__mark {
  width: 2.25rem;
  height: 1.52rem;
}

.gate h1 {
  font-size: clamp(1.75rem, 5vw, 2.25rem);
}

.gate p {
  color: var(--text-muted);
}

.signin {
  display: grid;
  gap: var(--space-2);
  margin: var(--space-6) 0 var(--space-4);
}

.signin .primary {
  margin-top: var(--space-2);
  width: 100%;
}

.chooser {
  list-style: none;
  margin: var(--space-6) 0 var(--space-4);
  padding: 0;
  display: grid;
  gap: var(--space-2);
}

.chooser button {
  width: 100%;
  justify-content: space-between;
  text-align: left;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-inset);
  border-color: var(--line-hairline);
  font-family: var(--font-body);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
}

/* ── Viewing as … ───────────────────────────────────────────────────────────
   The staff-view banner, and the one surface on this whole shell that must NOT
   look like it belongs to the client. Gold, not teal; a hard rule down the
   side; pinned above the chrome. Someone glancing at a support screen has to
   register in one look that they are inside somebody else's account and that
   nothing here is theirs to act on. */

.viewing {
  display: flex;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  background:
    linear-gradient(90deg, var(--wash-gold), transparent 42%),
    var(--surface-navy);
  border-bottom: 1px solid var(--line-hairline);
  border-left: 3px solid var(--accent-gold);
  font-size: 0.9rem;
}

.viewing__what {
  margin: 0;
  max-width: none;
}

.viewing__what strong {
  color: var(--accent-gold);
  font-family: var(--font-display);
  font-weight: 700;
}

/* The elastic member of the row. Letting the sentence absorb the slack keeps
   the clock and the way out together on the right — pushing the clock over with
   `margin-left: auto` instead ate the free space and wrapped the button onto a
   line of its own, below the thing it belongs to. */
.viewing__detail {
  flex: 1 1 12rem;
  margin: 0;
  max-width: none;
  color: var(--text-muted);
}

.viewing__clock {
  color: var(--warn-text);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  font-weight: 600;
  white-space: nowrap;
}

.viewing__exit {
  min-height: 2.5rem;
  padding: 0 var(--space-4);
  border-color: var(--line-strong);
  font-size: 0.78rem;
}

/* On a phone the sentence stops competing for the row, so the clock and the way
   out stay together rather than each claiming a line of their own. */
@media (max-width: 40rem) {
  .viewing__detail {
    flex: 1 1 100%;
  }
}

/* ── Website editor ─────────────────────────────────────────────────────────
   Phone-first: one column with an Edit/Preview toggle below 900px, two columns
   above it. The toggle is not a small-screen consolation — the product spec
   (L192–193) specifies a focused full-screen Edit/Preview workspace on mobile,
   because an owner editing from a phone should see one thing at a time. */

.editor__head {
  margin-bottom: var(--space-6);
}

.editor__tabs {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.editor__tabs button {
  flex: 1 1 auto;
  background: var(--surface-card);
}

.editor__tabs button[aria-selected="true"] {
  background: var(--surface-action);
  border-color: var(--line-teal);
  color: var(--text-strong);
}

.editor__body {
  display: grid;
  gap: var(--space-6);
}

/* Below 900px only the selected pane renders. */
.editor__body--edit .editor__preview,
.editor__body--preview .editor__form {
  display: none;
}

.editor__form {
  padding: var(--space-6);
  background: var(--surface-card);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-lg);
}

.field-group {
  margin-bottom: var(--space-8);
}

.field-group:last-child {
  margin-bottom: 0;
}

.field-group h3 {
  margin: 0 0 var(--space-4);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--line-hairline);
  font-family: var(--font-display);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.field {
  display: grid;
  gap: var(--space-2);
  margin-bottom: var(--space-6);
}

/* One sentence with something inside it. The client is shown their whole line,
   with the locked piece sitting in it, so they are editing a sentence and not
   two unlabelled fragments. */
.line__lede {
  margin: 0 0 var(--space-3);
}

.line__preview {
  margin: 0 0 var(--space-6);
  padding: var(--space-3);
  border: 1px solid var(--line-hairline);
  border-left: 3px solid var(--accent-teal);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  color: var(--text-primary);
  font-size: 1rem;
  line-height: 1.5;
}

/* The piece they cannot type over, shown in place so the sentence still reads
   as a sentence. Marked, not hidden — a gap here would look like a bug. */
.line__locked {
  padding: 0 0.25em;
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--text-muted);
  box-shadow: inset 0 0 0 1px var(--line-hairline);
}

.line__lockedrow {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
}

.line__lockedchip {
  padding: 0.15em 0.5em;
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--text-muted);
  box-shadow: inset 0 0 0 1px var(--line-hairline);
  font-size: 0.85rem;
}

.line__why {
  flex: 1 1 12rem;
  margin: 0;
  font-size: 0.85rem;
}

/* Find a line by what it says. One page's worth of inputs is still a few
   hundred to scroll past. Sticky, because what you are looking for is by
   definition not on screen. */
.fields__search {
  position: sticky;
  top: 0;
  z-index: 1;
  width: 100%;
  min-height: var(--tap-min);
  margin-bottom: var(--space-4);
  padding: 0 var(--space-3);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 1rem;
}

/* ── Draft vs live ──────────────────────────────────────────────────────────
   A segmented control over the preview. Both halves keep the full 48px tap
   target from the base button rule — a segmented control is where that floor
   is most often quietly dropped, and this one is used on a phone. */

.viewpick {
  display: inline-flex;
  padding: 3px;
  margin-bottom: var(--space-3);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
}

.viewpick__btn {
  border: 0;
  border-radius: var(--radius-sm);
  padding: 0 var(--space-4);
  background: transparent;
  color: var(--text-muted);
}

.viewpick .viewpick__btn:hover {
  background: transparent;
  color: var(--text-primary);
}

/* The selected half is raised out of the trough rather than filled with the
   accent: the accent is reserved for the primary ACTION on a view (§10), and
   choosing which version to look at is not one. */
.viewpick .viewpick__btn--on {
  background: var(--surface-card);
  color: var(--text-primary);
  box-shadow: inset 0 0 0 1px var(--line-strong);
}

.viewpick__note {
  margin: 0 0 var(--space-3);
  color: var(--text-muted);
  font-size: 0.875rem;
}

.rail .viewpick {
  margin: var(--space-2) 0;
}

/* `.preview` was a 100%-wide iframe — which is exactly the bug: 100% of a column
   capped at --measure is ~677px, so the client's site rendered its small-tablet
   layout and the desktop one was unreachable on any monitor. See lib/viewport.js.
   The frame now has a REAL fixed width and is scaled to fit. */
/* THE ONE THAT BITES. A `transform: scale()` shrinks what you SEE and not the
   box the parent reserves, so the 1280px scaler still contributes 1280px of
   min-content width. `.editor__preview` is a grid item, grid items default to
   `min-width: auto`, and auto means "no smaller than your content" — so the
   preview column blew out to 1282px and the whole editor scrolled sideways.
   Caught by measuring documentElement.scrollWidth against window.innerWidth;
   it is invisible in a screenshot until you look for the scrollbar. */
.editor__preview {
  min-width: 0;
}

.vp {
  position: relative;
  width: 100%;
  overflow: hidden;
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-lg);
  background: var(--text-strong);
}

/* Height and transform are set from JS, because the scale factor is a measured
   value. What must live here is the ORIGIN: with the default centre origin the
   scaled frame drifts up and left out of its own box. */
.vp__scaler {
  transform-origin: top left;
  will-change: transform;
}

.vp__frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  background: var(--text-strong);
}

.editor__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-6) 0 var(--space-2);
}

.health__line {
  font-size: 1.1rem;
  margin-bottom: var(--space-4);
}

/* Retained for any view still rendering a single service card rather than the
   tile grid. Same material as .tile so the two never diverge visually. */
.service-card {
  padding: var(--space-6);
  background: var(--surface-card);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-lg);
}

/* ── Review-before-publish ─────────────────────────────────────────────────── */

.modal__scrim {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--scrim);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  display: grid;
  place-items: center;
  padding: var(--space-4);
  overflow-y: auto;
}

.modal {
  position: relative;
  overflow: hidden;
  background: var(--surface-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  box-shadow: 0 32px 80px rgba(0, 0, 0, 0.55);
  padding: var(--space-8);
  max-width: 38rem;
  width: 100%;
}

.modal::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg, var(--accent-teal), rgba(79, 209, 192, 0) 58%);
}

.diff {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-4);
}

.diff li {
  border-top: 1px solid var(--line-hairline);
  padding: var(--space-4) 0;
}

.diff__label {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.01em;
  margin-bottom: var(--space-2);
}

.diff__was,
.diff__will {
  color: var(--text-muted);
  font-size: 0.8rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  margin: 0 0 var(--space-1);
}

.diff__value {
  margin: 0 0 var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-inset);
  border-left: 2px solid var(--line-hairline);
  border-radius: var(--radius-sm);
  font-size: 0.95rem;
}

/* The proposed wording gets the signal edge; the old wording does not. */
.diff__will + .diff__value {
  border-left-color: var(--accent-teal);
}

/* A photo change is shown as the photo. The path is not a thing a client can
   weigh up, and this is the screen where they decide to put it in front of
   their customers. */
/* ── What actually changed ──────────────────────────────────────────────────
   Two paragraphs to compare is a spot-the-difference puzzle at the moment a
   client is deciding what their customers will read. The words that move are
   marked in place instead. Colour is never the only signal — removed words are
   struck through and added words are underlined — because a review screen that
   only works for people who can distinguish red from green is not a review
   screen. */
.diff__gone,
.diff__added {
  padding: 0.05em 0.15em;
  border-radius: var(--radius-sm);
  background: transparent;
}

.diff__gone {
  color: var(--text-muted);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  background: var(--surface-raised);
}

.diff__added {
  color: var(--text-primary);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 2px;
  background: var(--wash-teal);
}

.diff__value--after { border-left-color: var(--accent-teal); }

/* ── Publishing, while it happens ───────────────────────────────────────────
   The dialog stays open and becomes this. Publishing waits for the site's own
   revision probe, so it takes seconds — long enough that a screen with no sign
   of life reads as a control that did nothing. */
.publishing {
  display: grid;
  justify-items: center;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-4);
  text-align: center;
}

/* No reduced-motion rule of its own: the blanket `animation: none !important`
   below already stops this, and adding a second one here pushed the global rule
   out of the proximity a test checks for. The panel still reads as working —
   the words carry the state and the ring stays visible, just still. */
.publishing__spinner {
  width: 2.25rem;
  height: 2.25rem;
  border: 3px solid var(--line-hairline);
  border-top-color: var(--accent-teal);
  border-radius: 50%;
  animation: publishing-spin 900ms linear infinite;
}

@keyframes publishing-spin {
  to { transform: rotate(360deg); }
}

.publishing__tick {
  width: 2.25rem;
  height: 2.25rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--accent-teal);
  color: var(--surface-inset);
  font-size: 1.2rem;
  font-weight: 700;
}

.publishing__line {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.05rem;
  color: var(--text-primary);
}

.publishing__sub {
  margin: 0;
  max-width: 34rem;
  font-size: 0.9rem;
}

.diff__value--photo {
  padding: var(--space-2);
}

.diff__photo {
  display: block;
  width: 100%;
  max-width: 18rem;
  height: 7rem;
  object-fit: cover;
  border-radius: var(--radius-sm);
}

/* Marks wording that comes from the site itself rather than from an edit the
   owner made. Muted, because the wording is the point and this is the footnote. */
.diff__origin {
  color: var(--text-muted);
  font-size: 0.85rem;
  text-transform: none;
  letter-spacing: 0;
}

/* The undo limit. Stated before publishing, every time. */
.review__warn {
  border-left: 3px solid var(--accent-gold);
  padding: var(--space-2) 0 var(--space-2) var(--space-4);
  background: linear-gradient(90deg, var(--wash-gold), transparent 60%);
  color: var(--text-primary);
}

.modal__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-8);
}

.no-target {
  display: grid;
  gap: var(--space-2);
}

/* The escape hatch under a disabled publish. Same geometry and same accent as
   `.footer__support` because it is the same support path — and `justify-self`
   keeps the 48px tap target from stretching across the whole grid column, where
   a full-width link would read as the primary action the disabled button above
   it is not. */
.no-target__support {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  justify-self: start;
  color: var(--accent-teal);
}

/* The armed form of the same escape hatch. The base `button` rule already gives
   it the icon gap and the 48px height; `justify-self` is here for the reason the
   link above has it — a full-width control under a disabled primary reads as the
   action that primary was going to be. */
.no-target__ask {
  justify-self: start;
}

/* The team-request dialog's field. The global input rule styles it; these two
   make it fill the dialog and sit under its own label rather than beside it. */
.review label {
  display: block;
  margin-top: var(--space-4);
  color: var(--text-muted);
  font-size: 0.875rem;
}

.review textarea {
  width: 100%;
  margin-top: var(--space-2);
}

@media (min-width: 900px) {
  .editor__body {
    grid-template-columns: minmax(22rem, 2fr) 3fr;
    align-items: start;
  }
  /* Both panes are visible on a wide screen, so the toggle stops hiding either. */
  .editor__body--edit .editor__preview,
  .editor__body--preview .editor__form {
    display: block;
  }
  .editor__tabs {
    display: none;
  }
}

/* ── The click-to-edit surface ──────────────────────────────────────────────
   Full-bleed on purpose. The subject of this screen is the client's own site,
   and every pixel of dashboard chrome around it is furniture competing with the
   thing they came to look at. `is-fullscreen` is set only while that surface is
   open, so the rest of the shell is untouched. */
body.is-fullscreen {
  margin: 0;
  overflow: hidden;
}

body.is-fullscreen #main {
  width: 100%;
  max-width: none;
  margin: 0;
  padding: 0;
}

/* The shell header is chrome too — it goes with the rest of the furniture. */
body.is-fullscreen #chrome,
body.is-fullscreen #site-footer {
  display: none;
}

/* The staff-view banner keeps its place above everything, including here. */
body.is-fullscreen #impersonation {
  position: relative;
  z-index: 30;
}

.bar__status,
.rail .status {
  margin: 0;
  font-size: 0.9rem;
  color: var(--text-muted);
}

.bar__status[data-tone="warn"],
.rail .status[data-tone="warn"] { color: var(--warn-text); }
.bar__status[data-tone="error"],
.rail .status[data-tone="error"] { color: var(--danger-text); font-weight: 600; }
.bar__status[data-tone="ok"],
.rail .status[data-tone="ok"] { color: var(--success-text); }

/* ── The tool belt ──────────────────────────────────────────────────────────
   Two layouts, one breakpoint (64rem, matching WIDE in direct-editor.js — keep
   them in step).

   WIDE: the belt is a left rail and edits happen inside it, so the client's page
   is never covered by the thing editing it. That is the whole reason the rail
   exists rather than a floating panel.

   NARROW: the rail collapses to a horizontal bar across the top and edits arrive
   as a bottom sheet. A side panel on a 375px viewport is just the page made
   unusable, and an anchored box vanishes under the keyboard the moment anyone
   types into it. */

.stage {
  display: flex;
  flex-direction: column;
  height: 100svh;
}

.rail {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  background:
    linear-gradient(115deg, rgba(79, 209, 192, 0.05), transparent 46%),
    var(--surface-navy);
  border-bottom: 1px solid var(--line-hairline);
  flex: 0 0 auto;
}

/* The same vein as the shell header, so the editor reads as the same product. */
.rail::after {
  content: "";
  position: absolute;
  inset: auto 0 -1px 0;
  height: 1px;
  background: linear-gradient(90deg, var(--accent-teal), rgba(79, 209, 192, 0) 62%);
  opacity: 0.7;
  pointer-events: none;
}

.rail__title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.015em;
}

/* Why the publish above it is greyed out. Sits in the belt with the controls
   rather than in the status line, because the status line is where saving talks
   and this has to stay put while it does. */
.rail__why {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.875rem;
}

.rail__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-left: auto;
}

/* Empty on narrow screens — the card mounts to the viewport instead. */
.rail__edit:empty {
  display: none;
}

.stage__body {
  flex: 1 1 auto;
  min-height: 0;
  background: var(--surface-page);
}

.stage__frame {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* NARROW: the belt is a top bar, and a wrapping row of five uppercase buttons
   grew tall enough to leave a phone showing almost none of the site the surface
   exists to display. One non-wrapping row that scrolls sideways instead: the
   48px floor is untouched, only the horizontal padding and label size give. */
@media (max-width: 63.999rem) {
  .rail {
    padding: var(--space-2) var(--space-3);
  }

  .rail__actions {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    width: 100%;
    margin-left: 0;
  }

  .rail__actions button {
    flex: 0 0 auto;
    padding: 0 var(--space-3);
    font-size: 0.75rem;
  }
}

@media (min-width: 64rem) {
  .stage {
    flex-direction: row;
  }

  .rail {
    flex-direction: column;
    align-items: stretch;
    flex: 0 0 22rem;
    /* Its own scroll: a long edit panel must not push the tool belt off-screen,
       and the page beside it must not move at all. */
    overflow-y: auto;
    padding: var(--space-6);
    border-bottom: 0;
    border-right: 1px solid var(--line-hairline);
    height: 100svh;
  }

  /* The vein runs down the inner edge when the belt is vertical. */
  .rail::after {
    inset: 0 -1px 0 auto;
    width: 1px;
    height: auto;
    background: linear-gradient(180deg, var(--accent-teal), rgba(79, 209, 192, 0) 62%);
  }

  .rail__actions {
    display: grid;
    gap: var(--space-2);
    margin-left: 0;
  }

  .rail__edit {
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--line-hairline);
  }

  /* Inside the rail the card is just content — no fixed position, no shadow, no
     backdrop. It is part of the belt, not something laid over the page. */
  /* `.sheet.sheet--railed` rather than `.sheet--railed`: both are one class, and
     a media query adds no specificity, so the plain-class version LOSES to the
     base `.sheet` rule declared later in this file — the card kept
     `position: fixed` and floated over the page, which is the exact thing the
     rail exists to prevent. Doubling the class outranks the base rule without
     moving any block and flipping the cascade for everything else. */
  .sheet.sheet--railed {
    position: static;
    width: auto;
    max-height: none;
    border: 0;
    box-shadow: none;
    padding: 0;
    background: none;
    overflow: visible;
  }

  .sheet.sheet--railed::before {
    display: none;
  }

  .sheet.sheet--railed .sheet__actions {
    justify-content: flex-start;
  }
}

/* The editing card. A bottom sheet on phones, because a floating box anchored
   to an element becomes unreachable once the keyboard covers half the screen. */
.sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  display: grid;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-4) var(--space-4);
  background: var(--surface-card);
  border-top: 1px solid var(--line-strong);
  box-shadow: 0 -18px 48px rgba(0, 0, 0, 0.5);
  max-height: 70svh;
  overflow-y: auto;
}

.sheet::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: linear-gradient(90deg, var(--accent-teal), rgba(79, 209, 192, 0) 62%);
}

.sheet__label {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.015em;
}

.sheet__hint {
  margin: 0;
  font-size: 0.85rem;
  color: var(--text-muted);
}

.sheet__input {
  width: 100%;
  /* 16px minimum: see the note on `input` above — smaller zooms iOS Safari. */
  font-size: 1rem;
  padding: var(--space-3);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  color: var(--text-primary);
  font-family: var(--font-body);
}

.sheet__actions {
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
}

/* `.picker` lived here — the per-slot photo grid, drawn once for every image
   slot on the site. It is gone rather than kept "just in case": nothing renders
   it, and a stylesheet that describes controls the app no longer has is how the
   next person spends an afternoon looking for a screen that does not exist. Its
   replacement is `.library` below. */

/* Wide screens: the sheet becomes a panel rather than spanning the display. */
@media (min-width: 48rem) {
  .sheet {
    left: auto;
    right: var(--space-6);
    bottom: var(--space-6);
    width: min(28rem, calc(100vw - 4rem));
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-lg);
    max-height: 70vh;
  }

  .sheet--wide {
    width: min(40rem, calc(100vw - 4rem));
  }
}

/* ── The photo library ──────────────────────────────────────────────────────
   One grid for the whole site, mounted by all three editing surfaces. It
   replaces the per-slot pickers, which drew the same fifty-four thumbnails once
   for every image slot on the page. The review dialog is unaffected — it shows
   a before/after pair through `.diff__photo`, not through a picker. */

.library {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  /* Mounted inside a grid in the sheet and inside a flex column elsewhere. In
     both, the default minimum size is the CONTENT size, so without this a wide
     photo grid widens its container rather than reflowing into fewer columns —
     which on a phone is a page that scrolls sideways. */
  min-width: 0;
}

.library__target {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}

.library__select {
  flex: 1 1 14rem;
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  color: var(--text-primary);
  font-family: var(--font-body);
  /* 16px: anything smaller and iOS Safari zooms the whole page on focus, which
     on the click-to-edit surface throws the client out of their own site. */
  font-size: 1rem;
}

.library__add {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.library__hint,
.library__count {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.85rem;
}

.library__search {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.library__filter {
  flex: 1 1 12rem;
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 1rem;
}

.library__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.library__item {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* The whole tile is the control. A separate "choose" button under each photo
   would double the number of things to aim at on a phone for no added meaning —
   the photo IS the choice. */
.library__pick {
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  min-height: 0;
  padding: 0;
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  overflow: hidden;
  text-transform: none;
  letter-spacing: normal;
}

.library__pick img {
  display: block;
  width: 100%;
  height: 6rem;
  object-fit: cover;
}

.library__name {
  padding: var(--space-2);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 0.8rem;
  font-weight: 400;
  line-height: 1.3;
  text-align: left;
  /* Two lines, then ellipsis. A long filename must not make one tile twice the
     height of its neighbours and break the grid's rhythm. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Selected state. NEVER carried by the border alone — `.library__mark--on`
   spells it out underneath, and `aria-pressed` carries it for assistive tech
   (§10: colour is never the only signal). */
.library__pick--on {
  border-color: var(--accent-teal);
  box-shadow: 0 0 0 1px var(--accent-teal);
}

.library__missing {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 6rem;
  padding: var(--space-2);
  background: var(--surface-inset);
  color: var(--text-muted);
  font-size: 0.75rem;
  text-align: center;
}

.library__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
}

.library__mark {
  color: var(--text-muted);
  font-size: 0.72rem;
  line-height: 1.3;
}

.library__mark--on {
  color: var(--accent-teal);
  font-weight: 700;
}

.library__mark--mine {
  color: var(--accent-gold);
}

/* Smaller than the 48px floor on purpose, and allowed to be: removing a photo
   is a secondary action inside a tile whose PRIMARY target is the full-size
   photo above it, and SC 2.5.8 exempts a control that sits inline with the
   thing it acts on. Making it 48px would put a button the size of the
   thumbnail under every thumbnail. */
.library__remove {
  min-height: 0;
  padding: var(--space-1) var(--space-2);
  font-size: 0.72rem;
  letter-spacing: normal;
  /* Sentence case, unlike every other button here. This is the one control on
     the screen that throws something away, and shouting REMOVE under a photo
     reads as the tile's purpose rather than its footnote. */
  text-transform: none;
}

/* Reachable, unseen. The file input has to stay in the accessibility tree and
   in the tab order — `display: none` would take it out of both and break the
   label that drives it. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The library inside the field-list editor, where it sits under the words. */
.editor__photos {
  margin-top: var(--space-3);
}

/* The library inside the click-to-edit sheet.
   `min-width: 0` rather than an overflow rule: `.sheet` already scrolls at
   70svh, and a second scroller nested inside it would be worse than none. What
   the sheet does NOT do is let a grid item shrink below its content — the
   default `min-width: auto` on a grid child is what makes a photo grid push
   the sheet sideways instead of reflowing into fewer columns. */
.sheet__body {
  min-width: 0;
}

/* The SEO caution in the review dialog. Informative, not alarming: this is
   advice a client should read and weigh, not an error they have to clear. It is
   shown only when we KNOW the client has the service. */
.review__seo {
  margin: 0 0 var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-left: 2px solid var(--accent-teal);
  background: var(--surface-inset);
  font-size: 0.92rem;
}

/* ── The studio ─────────────────────────────────────────────────────────────
   The client's site, full screen, with the tools floating over it. The frame is
   mounted once and never rebuilt (see stage.js), so nothing here may depend on
   a re-render.

   The flex column on #root is what lets the staff-view banner keep its place
   above the studio without pushing the dock off the bottom of the screen — the
   studio takes the space that is left rather than a flat 100svh. */

body.is-fullscreen #root:has(.studio) {
  display: flex;
  flex-direction: column;
  height: 100svh;
}

body.is-fullscreen #root:has(.studio) #main {
  flex: 1 1 auto;
  min-height: 0;
}

.studio {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  background: var(--surface-page);
}

/* The page, and everything that floats over the page. Every absolutely
   positioned tool below is placed against THIS box rather than the whole
   studio, so none of them has to know how tall the bar is. */
.studio__canvas {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

/* The width, the height, the offset and any scale are set by `applyWidth` in
   stage.js — they are measurements, and CSS cannot measure. What is here is
   what does NOT change: an absolutely positioned box with `left`, `right` AND
   `width` set is over-constrained and one of them is silently dropped, so
   `right` is released for the width to mean anything at all; and the transform
   origin is pinned because the default `50% 50%` slides a scaled frame up and
   out of its own box, which still looks like a preview, just cropped wrongly. */
.studio__frame {
  position: absolute;
  inset: 0;
  right: auto;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
  transform-origin: top left;
}

/* Phone width gets an edge, so a narrow page on a dark field reads as a phone
   rather than as a page that failed to load the rest of itself. */
.studio--phone .studio__frame {
  box-shadow: 0 0 0 1px var(--line-strong);
}

/* Stands in for the page when there is no page to show — a staff view cannot
   mint a preview, and an unexplained black rectangle reads as a broken screen. */
.studio__void {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--space-3);
  padding: var(--space-8);
  text-align: center;
  background: var(--surface-page);
}

.studio__void-glyph {
  width: 2rem;
  height: 2rem;
  color: var(--text-muted);
  opacity: 0.7;
}

.studio__void-line {
  margin: 0;
  max-width: 32ch;
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.studio__void .muted {
  margin: 0;
  max-width: 40ch;
  font-size: 0.9rem;
}

/* A strip, not an overlay — the same shape the CRM's sticky header has, and for
   the same reason: it carries context, and context that hides the client's own
   navigation is not context, it is a lid. The floating thing is the dock. */
.studio__bar {
  position: relative;
  z-index: 3;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-2) var(--space-3);
  padding-top: calc(var(--space-2) + env(safe-area-inset-top));
  background:
    linear-gradient(115deg, rgba(79, 209, 192, 0.06), transparent 46%),
    var(--glass-navy);
  backdrop-filter: blur(14px) saturate(130%);
  -webkit-backdrop-filter: blur(14px) saturate(130%);
  border-bottom: 1px solid var(--line-hairline);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .studio__bar { background: var(--surface-navy); }
}

.studio__save {
  flex: 1 1 12rem;
  min-height: 0;
  margin: 0;
  color: var(--text-muted);
  font-size: 0.85rem;
}

.studio__save[data-tone="ok"] { color: var(--success-text); }
.studio__save[data-tone="warn"] { color: var(--warn-text); }
.studio__save[data-tone="error"] { color: var(--danger-text); font-weight: 600; }

/* Undo lives beside the version it acts on, and never moves. Disabled rather
   than hidden when there is nothing to take back: a control that comes and goes
   is harder to learn than one that is always in the same place. */
.studio__undo {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  font-size: 0.72rem;
}

.studio__undo[disabled] { opacity: 0.4; }

.studio__undo-glyph { width: 1rem; height: 1rem; }

.studio__width {
  display: flex;
  flex: none;
}

/* Which page. Empty — and so invisible — until the site says it has more than
   one page the editor may open.

   GROUPED since 2026-08-12: the founder could not find the Sunrooms page in a
   flat list of 32 sorted by path and labelled with their SEO titles, and
   reported it missing. The headings come from the site. */
.studio__pages {
  display: flex;
  flex: none;
}

.studio__page {
  max-width: 13rem;
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  color: var(--text-primary);
  font-family: var(--font-body);
  /* 16px, for the reason `.library__select` gives: anything smaller and iOS
     Safari zooms the whole page on focus, which throws the client out of the
     site they are standing in. */
  font-size: 1rem;
}

/* The headings. Deliberately only weight and colour: how far a browser lets an
   `<optgroup>` be styled varies, and every one of them draws the label in some
   distinct way already — so this nudges the native rendering rather than trying
   to replace it, which is the way to end up with a heading that is invisible on
   one platform. The options are re-stated at full contrast because inheriting
   the dimmed label colour would make the pages harder to read than the headings
   above them, which is backwards. */
.studio__page optgroup {
  font-weight: 600;
  color: var(--text-muted);
  background: var(--surface-inset);
}

.studio__page option {
  font-weight: 400;
  color: var(--text-primary);
  background: var(--surface-inset);
}

.studio__exits {
  display: flex;
  gap: var(--space-2);
  margin-left: auto;
}

.studio__exits button {
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  font-size: 0.72rem;
}

/* On a phone the sentence gets its own row rather than squeezing the controls
   below a usable width — and the width picker goes, because a 1280px desktop
   layout shrunk to fit a 375px screen is a third of full size and legible to
   nobody. Nothing is lost: the frame already renders at the phone width there,
   which is the answer the control exists to give. Three rows of chrome on the
   one screen whose subject is the page underneath is the worse trade. */
@media (max-width: 34rem) {
  .studio__save {
    order: 3;
    flex: 1 1 100%;
  }

  .studio__width { display: none; }
}

/* The two segmented groups in the bar.
   `.viewpick` already carries its own margin for the column layout it was born
   in; in a flex bar that margin is a half-row of dead space under the buttons. */
.studio__bar .viewpick {
  margin-bottom: 0;
  flex: none;
}

/* Colour AND word. This pair replaced a badge that NAMED the version beside a
   button that left the screen to change it; the lit segment now does both, and
   it inherits the badge's tones so the version is still never carried on hue
   alone — the label is a word and `aria-pressed` says which. */
.viewpick .viewpick__btn--draft {
  color: var(--accent-teal);
  background: var(--wash-teal);
  box-shadow: inset 0 0 0 1px var(--line-teal);
}

.viewpick .viewpick__btn--live {
  color: var(--accent-gold);
  background: var(--wash-gold);
  box-shadow: inset 0 0 0 1px var(--line-gold);
}

/* A bottom-anchored wash rather than a flat scrim: the panel and the dock need
   contrast under them, but the client came here to look at their page and
   dimming the whole thing to read a form is the wrong trade. */
.studio__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(
    to top,
    rgba(4, 7, 11, 0.86) 0%,
    rgba(4, 7, 11, 0.4) 40%,
    transparent 70%
  );
}

/* ── The dock ───────────────────────────────────────────────────────────────
   The tab bar of tools, floating over the page rather than taking a column off
   it. Draft only — live mode has no editing tools at all, not disabled ones. */

.dock {
  position: absolute;
  left: 50%;
  bottom: calc(var(--space-4) + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  gap: var(--space-1);
  max-width: calc(100vw - 1.5rem);
  padding: var(--space-1);
  background: var(--glass-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
}

.dock::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg, var(--accent-teal), rgba(79, 209, 192, 0) 68%);
  opacity: 0.8;
  pointer-events: none;
}

.dock__tab {
  flex-direction: column;
  gap: 0.15rem;
  min-width: 4.5rem;
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-1);
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-muted);
  font-size: 0.66rem;
  letter-spacing: 0.08em;
}

.dock .dock__tab:hover {
  background: var(--wash-teal);
  color: var(--text-primary);
}

.dock .dock__tab--on {
  background: var(--surface-raised);
  color: var(--text-primary);
  box-shadow: inset 0 0 0 1px var(--line-strong), 0 0 20px var(--glow-teal);
}

.dock__glyph {
  width: 1.15rem;
  height: 1.15rem;
}

.dock .dock__review {
  background: var(--surface-action);
  color: var(--text-strong);
  box-shadow: inset 0 0 0 1px var(--line-teal);
}

.dock .dock__review:hover {
  background: var(--surface-action);
  box-shadow: inset 0 0 0 1px var(--accent-teal), 0 0 22px var(--glow-teal);
}

.dock .dock__review[disabled] {
  background: transparent;
  color: var(--text-muted);
  box-shadow: none;
  opacity: 0.5;
}

/* The number of changes waiting. Empty when there are none, so the badge is
   never a zero asking to be interpreted. */
.dock__count:empty { display: none; }

.dock__count {
  position: absolute;
  top: -0.35rem;
  right: -0.35rem;
  min-width: 1.15rem;
  padding: 0 0.25rem;
  border-radius: var(--radius-pill);
  background: var(--accent-teal);
  color: var(--surface-page);
  font-size: 0.66rem;
  font-weight: 700;
  line-height: 1.15rem;
}

.dock .dock__review { position: relative; }

/* The primary action reads on ONE line, icon beside label rather than above it.
   The three tabs stay column-stacked and narrow; this one is a verb, not a
   destination, and "Review & publish" wrapped into a 4.5rem column would both
   look broken and push the dock past the viewport on a phone. */
.dock .dock__review {
  flex-direction: row;
  gap: var(--space-2);
  white-space: nowrap;
}

/* One label at a time. The wide phrase is the default; the short one appears
   only where four dock items would otherwise overflow the viewport. */
.dock__label--narrow { display: none; }

@media (max-width: 30rem) {
  .dock__label--wide { display: none; }
  .dock__label--narrow { display: inline; }
}

@media (max-width: 22rem) {
  .dock__tab { min-width: 4rem; }
}

/* ── The panel ──────────────────────────────────────────────────────────────
   NARROW: rises from the dock as a sheet. A side panel on a 375px viewport is
   just the page made unusable, and an anchored box vanishes under the keyboard
   the moment anyone types into it.

   WIDE (≥64rem, the same breakpoint direct-editor.js uses — keep them in step):
   a rail down the left, FLOATING over the page rather than taking a column off
   it. The sheet is centred, which on a wide monitor means it covers the middle
   of the very page it is editing — you cannot watch a headline change while the
   box changing it sits on top of it. The rail moves the tools off to one side
   and leaves the site full-bleed underneath. */

.panel {
  position: absolute;
  left: 50%;
  bottom: calc(var(--space-4) + env(safe-area-inset-bottom) + 4.5rem);
  transform: translateX(-50%);
  z-index: 5;
  width: min(40rem, calc(100vw - 1.5rem));
  max-height: 60svh;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--glass-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  box-shadow: 0 28px 70px rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  animation: panel-rise 200ms ease-out both;
}

@keyframes panel-rise {
  from { opacity: 0; transform: translate(-50%, 0.75rem); }
  to { opacity: 1; transform: translate(-50%, 0); }
}

@media (min-width: 64rem) {
  .panel {
    left: var(--space-4);
    right: auto;
    /* Against the canvas, so the bar's height is not a number repeated here. */
    top: var(--space-4);
    bottom: calc(var(--space-4) + env(safe-area-inset-bottom) + 4.5rem);
    transform: none;
    width: 26rem;
    max-height: none;
    /* `panel-rise` translates -50% on X to undo the sheet's centring. The rail
       is not centred, so that same keyframe would shove it half a panel-width
       off the left edge. */
    animation-name: rail-in;
  }

  @keyframes rail-in {
    from { opacity: 0; transform: translateX(-0.75rem); }
    to { opacity: 1; transform: none; }
  }

  /* Step out from under the rail rather than sitting behind it. Centred on the
     space that is actually left, so the tools stay under the part of the page
     they are editing. */
  .studio:has(.panel:not([hidden])) .dock {
    left: calc(50% + 13rem + var(--space-4));
  }

  /* And the phone with it — a 390px page centred on the whole canvas slides
     under the rail, which is the one thing on screen the rail must not cover.
     A margin rather than a `left`, because `left` is measured in stage.js and
     this must compose with it rather than fight it. */
  .studio--phone:has(.panel:not([hidden])) .studio__frame {
    margin-left: calc(13rem + var(--space-4));
  }

  /* The wash is anchored to wherever the tools are. With a rail on the left,
     a bottom-up gradient puts the contrast under nothing and dims the page
     for no reason. */
  .studio:has(.panel:not([hidden])) .studio__scrim {
    background: linear-gradient(
      to right,
      rgba(4, 7, 11, 0.82) 0%,
      rgba(4, 7, 11, 0.34) 34%,
      transparent 58%
    );
  }
}

.panel__head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4) var(--space-3);
  background: var(--glass-card);
  border-bottom: 1px solid var(--line-hairline);
}

.panel__title {
  margin: 0;
  font-size: 1rem;
  letter-spacing: -0.01em;
}

.panel__close {
  min-height: 2.5rem;
  padding: 0 var(--space-4);
  font-size: 0.72rem;
}

.panel__body {
  padding: var(--space-4);
}

.panel__body .field-group:last-child { margin-bottom: 0; }

.panel__actions {
  display: grid;
  gap: var(--space-2);
}

/* The support path (§10: one is always visible). Every other screen gets it
   from the persistent footer — and the footer is precisely what full screen
   hides, so this surface has to carry its own. */
.panel__support {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  margin-top: var(--space-3);
  color: var(--text-muted);
  font-size: 0.85rem;
}

.panel__support:hover { color: var(--text-primary); }

/* ── The client's three saved versions ──────────────────────────────────────
   Rows, not cards. Three cards in a sheet that already carries the publish
   escape hatch pushes "Throw away my changes" off the bottom, and the control
   someone opened this panel in a hurry for must not be the one that scrolls
   away. The rule above the block is what separates them from the one-off
   actions, so nobody reads Restore as a fourth button in that list. */
.vslots {
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line-hairline);
}

.vslots__head {
  margin: 0 0 var(--space-2);
  font-family: var(--font-display);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.vslots__list {
  display: grid;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

/* Wraps rather than truncating: a client's own name for a version is the whole
   point of letting them type one, and an ellipsis in the middle of "before the
   new photos" is a slot they can no longer tell from the next. */
.vslot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
}

/* The row that is LIVE, in the publish list. One border, no badge and no fill:
   the row already says "This is what your site shows now" in words, and colour
   is never the only signal (§10). It is the one row with no button, so the
   outline is what stops that reading as a row whose control failed to draw. */
.vslot--now {
  border-color: var(--accent-teal);
}

.vslot__what {
  min-width: 0;
  flex: 1 1 12rem;
}

.vslot__name {
  margin: 0;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.vslot__when {
  margin: var(--space-1) 0 0;
  font-size: 0.8rem;
}

.vslot__acts {
  display: flex;
  gap: var(--space-2);
}

/* Smaller than a panel action and still 48px tall: §10's tap floor is not a
   thing a compact row gets to opt out of. */
.vslot__act {
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  font-size: 0.85rem;
}

.vslot__act:hover {
  border-color: var(--accent-teal);
}

/* `.field-group--target` lived here — it highlighted the photo slot the client
   had clicked on the page, back when the Photos panel was one section per slot.
   The library says the same thing better, in words: the chooser above the grid
   NAMES the spot being filled. Deleted rather than kept, for the reason
   `.picker` was: a rule nothing renders is a screen nobody can find. */

/* ── Arrival ────────────────────────────────────────────────────────────────
   One short stagger as a view lands, and only on structural blocks — never
   under body copy (§10). It runs once; nothing here loops or reacts to scroll. */

@keyframes rise {
  from { opacity: 0; transform: translateY(0.5rem); }
  to { opacity: 1; transform: none; }
}

/* ── Your AI Brain ───────────────────────────────────────────────────────────
   The one screen in this shell whose job is partly to be impressive. §10 rules
   out most of the usual ways — no WebGL, no scrolljacking, no carousels, no
   autoplay, no motion under body copy — so the weight is carried by real data
   drawn well: a ring that is a count, a map whose spokes exist only where the
   brain has something, and provenance lines under values we found ourselves.

   One column for the form at every width. A two-column form is the thing that
   makes a template read as an admin panel, and line length beats density for
   something an owner reads on a phone half the time. */

.brainpromo {
  padding: var(--space-6);
  border: 1px solid var(--line-teal);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(ellipse 70% 120% at 100% 0%, var(--glow-teal-deep), transparent 70%),
    var(--surface-card);
}

.brainpromo__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}

.brainpromo__head .eyebrow {
  margin-bottom: 0;
}

.brainpromo__pct {
  margin: 0;
  color: var(--accent-teal);
  font-family: var(--font-display);
  font-weight: 700;
}

.brainpromo__verdict {
  margin: var(--space-2) 0 0;
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 1rem + 1.2vw, 1.75rem);
  color: var(--text-strong);
}

.brainpromo__powers {
  margin: var(--space-1) 0 0;
}

.brainpromo__note {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  color: var(--warn-text);
}

.brainpromo__open {
  margin-top: var(--space-4);
}

/* ── The surface ─────────────────────────────────────────────────────────── */

.brain__head {
  margin-bottom: var(--space-8);
}

.brain__sub {
  max-width: 46ch;
  margin: 0 0 var(--space-2);
  color: var(--text-primary);
  font-size: 1.05rem;
}

.brain__nopublish {
  margin: 0;
  font-size: 0.9rem;
}

.brain__notice {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  color: var(--text-primary);
}

.brain__notice--warm {
  border-color: var(--line-gold);
  background: var(--wash-gold);
}

.brain__body {
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 64rem) {
  .brain__body {
    grid-template-columns: 20rem minmax(0, 1fr);
    align-items: start;
  }

  .brain__rail {
    position: sticky;
    top: var(--space-12);
  }
}

.brain__main {
  max-width: 42rem;
}

/* ── The rail: ring, map, destinations, contents ─────────────────────────── */

.brain__gauge {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.ring {
  width: 6.5rem;
  height: 6.5rem;
  flex: 0 0 auto;
  overflow: visible;
}

.ring__track,
.ring__value {
  fill: none;
  stroke-width: 6;
}

.ring__track {
  stroke: var(--ring-track);
}

/* The arc carries its real value in the markup; this only reveals it. `from`
   alone means the `to` state is the element's own computed value, so the
   keyframe never has to know the number — and the reduced-motion block at the
   end of this file kills it with `animation: none !important`. */
@keyframes ring-fill {
  from { stroke-dashoffset: var(--ring-c); }
}

.ring__value {
  stroke: var(--accent-teal);
  stroke-linecap: butt;
  animation: ring-fill var(--motion) ease-out both;
}

.ring__tick {
  stroke: var(--ring-track);
  stroke-width: 2;
}

.ring__tick--done {
  stroke: var(--accent-teal);
}

.ring__label {
  fill: var(--text-strong);
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The caption is a BUTTON, because the number is the thing people query. "2 of
   12" when you typed one thing reads as a miscount until the screen can be asked
   to show its working. */
.brain__gaugeline {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  min-height: var(--tap-min);
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-body);
  font-size: 0.9rem;
  text-align: left;
  cursor: pointer;
  /* The shell styles every `button` as a control: uppercase, tracked out, bold.
     Right for "ANSWER THIS", wrong here — this is a sentence, and making it a
     button to get the disclosure turned "9 from you, 2 we found" into a shouted
     banner. It reads as a caption again. */
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
}

.brain__gaugeline:hover {
  color: var(--text-primary);
}

.brain__gaugemore {
  color: var(--accent-teal);
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
}

.brain__checklist {
  margin: var(--space-4) 0 var(--space-6);
  padding: var(--space-4);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
}

.brain__checkitems {
  margin: 0 0 var(--space-3);
  padding: 0;
  list-style: none;
}

.brain__checkitem {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-1) 0;
  font-size: 0.9rem;
  line-height: 1.45;
}

.brain__checkitem--todo {
  color: var(--text-muted);
}

.brain__checkitem--you {
  color: var(--text-primary);
}

/* Amber, the same tone the shell reserves for "read this". A value nobody has
   checked is not a finished one, and it is the state most worth acting on:
   wrong here is worse than missing, because the AI says it with confidence. */
.brain__checkitem--found {
  color: var(--text-primary);
}

.brain__checkfrom {
  color: var(--warn-text);
}

/* On a phone the rail stacks ABOVE the form, so every pixel of it is scrolling
   the owner has to do before they can type. Full size on the desktop rail where
   it costs nothing; half on a phone, where the map still says "it knows six
   areas and one is empty" at a glance and gives the field back. */
.brainmap {
  display: block;
  width: 100%;
  max-width: 11rem;
  margin: var(--space-4) auto;
}

@media (min-width: 64rem) {
  .brainmap {
    max-width: 20rem;
    margin: var(--space-6) 0;
  }
}

.brainmap__grid {
  fill: none;
  stroke: var(--map-grid);
  stroke-width: 1;
}

/* ── The map, alive ─────────────────────────────────────────────────────────
   Everything below is CSS keyframes. No requestAnimationFrame anywhere near
   this surface: rAF does not fire in an unpainted document, and the
   reduced-motion block at the foot of this file kills `animation` with
   `!important`, so honouring that preference is free and cannot be forgotten.

   Slow on purpose. `--motion` (180ms) is the interaction budget — the speed a
   control responds at. Ambient motion runs two orders of magnitude slower, so
   the map reads as something running rather than something loading. Nothing
   here sits under body copy, which is what §10 actually forbids. */

/* Dashed, because a rotating solid circle is a still circle. Counter-rotating
   so the two rings never lock into one apparent shape. */
.brainmap__orbit {
  fill: none;
  stroke: var(--line-hairline);
  stroke-width: 1;
  stroke-dasharray: 3 9;
  transform-origin: 160px 160px;
}

.brainmap__orbit--inner {
  animation: brain-orbit 68s linear infinite;
}

.brainmap__orbit--outer {
  animation: brain-orbit 96s linear infinite reverse;
}

@keyframes brain-orbit {
  to { transform: rotate(360deg); }
}

.brainmap__link {
  stroke: var(--map-link);
  stroke-width: 1;
}

/* A short bright segment travelling node → core. `--len` is the spoke length
   plus its own dash, handed over by brain-map.js because only it knows the
   geometry; `--beat` staggers each spoke so they do not pulse in lockstep. */
.brainmap__pulse {
  stroke: var(--accent-teal);
  stroke-width: 1.5;
  stroke-linecap: round;
  animation: brain-pulse 4.6s cubic-bezier(0.4, 0, 0.5, 1) infinite;
  animation-delay: var(--beat, 0s);
}

@keyframes brain-pulse {
  0%        { stroke-dashoffset: var(--len, 100); opacity: 0; }
  12%       { opacity: 0.9; }
  70%, 100% { stroke-dashoffset: 0; opacity: 0; }
}

.brainmap__node--full {
  fill: var(--accent-teal);
  stroke: none;
}

.brainmap__node--learned {
  fill: none;
  stroke: var(--text-muted);
  stroke-width: 1;
  stroke-dasharray: 2 2;
}

.brainmap__node--empty {
  fill: none;
  stroke: var(--map-node-empty);
  stroke-width: 1;
  stroke-dasharray: 2 2;
}

/* Filled areas glow gently; empty ones stay still and dim. The eye is drawn to
   what the brain HAS, never nagged toward what it lacks — the guide does that
   job, in words, where it can also say what to type. */
.brainmap__node--full {
  animation: brain-node 6.2s ease-in-out infinite;
  animation-delay: var(--beat, 0s);
}

@keyframes brain-node {
  0%, 100% { opacity: 0.82; }
  50%      { opacity: 1; }
}

.brainmap__satellite {
  fill: var(--map-link);
  animation: brain-twinkle 5.4s ease-in-out infinite;
  animation-delay: var(--beat, 0s);
}

@keyframes brain-twinkle {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 1; }
}

/* The core breathes. Slowest thing on the surface, and the only one that moves
   when the brain is completely empty — so a client with nothing saved still
   sees something alive waiting for them. */
.brainmap__glow {
  fill: var(--glow-teal-deep);
  transform-origin: 160px 160px;
  animation: brain-breathe 7.8s ease-in-out infinite;
}

@keyframes brain-breathe {
  0%, 100% { transform: scale(1);    opacity: 0.85; }
  50%      { transform: scale(1.07); opacity: 1; }
}

.brainmap__core {
  fill: var(--surface-raised);
  stroke: var(--line-teal);
  stroke-width: 1;
}

.brain__powers {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-6);
}

.brain__powers .eyebrow {
  width: 100%;
  margin-bottom: 0;
}

.brain__nav {
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--line-hairline);
}

.brain__navitem {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-1);
  border: 0;
  border-bottom: 1px solid var(--line-hairline);
  background: transparent;
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 0.95rem;
  text-align: left;
  cursor: pointer;
}

.brain__navitem:hover {
  color: var(--accent-teal);
}

.brain__navcount {
  color: var(--text-muted);
}

/* ── Sections and rows ───────────────────────────────────────────────────── */

/* ── The guide ───────────────────────────────────────────────────────────────
   One ask at a time. The card is the loudest thing in the column on purpose:
   an owner opening this screen should have exactly one obvious place to start,
   and the folded sections behind it should read as "later", not as "ignored". */

.guide {
  margin-bottom: var(--space-8);
  padding: var(--space-6);
  border: 1px solid var(--line-teal);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(ellipse 80% 130% at 0% 0%, var(--glow-teal-deep), transparent 68%),
    var(--surface-card);
}

.guide__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}

.guide__head .eyebrow {
  margin-bottom: 0;
}

.guide__count {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.85rem;
}

.guide__headline {
  margin: var(--space-2) 0 var(--space-2);
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 1rem + 1vw, 1.6rem);
  color: var(--text-strong);
  text-wrap: balance;
}

.guide__why {
  max-width: 52ch;
  margin: 0 0 var(--space-4);
  color: var(--text-primary);
}

/* The example is the most useful thing on the card — owners copy the shape of
   what they are shown — so it is set apart rather than buried in the prose. */
.guide__example {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-left: 2px solid var(--line-teal);
  background: var(--wash-teal);
  color: var(--text-primary);
}

.guide__example-label {
  color: var(--text-muted);
}

.guide__draft {
  margin-top: var(--space-4);
  padding: var(--space-4);
  border: 1px dashed var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--wash-learned);
}

.guide__drafttext {
  margin: 0 0 var(--space-2);
  color: var(--text-primary);
}

.guide__draftfrom {
  margin: 0 0 var(--space-3);
  color: var(--text-muted);
  font-size: 0.85rem;
}

.guide__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.guide--done .guide__headline {
  color: var(--success-text);
}

/* ── Folded sections ─────────────────────────────────────────────────────── */

.brain__fold {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: 0 var(--space-2);
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-family: var(--font-body);
  font-size: 0.9rem;
  cursor: pointer;
}

.brain__fold:hover {
  color: var(--accent-teal);
}

/* The word, not just the chevron — a fold state is never carried by a glyph
   alone (colour and shape are never the only signal, and neither is rotation). */
.brain__foldword {
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* A folded section still shows its heading and its count, so the client can see
   the whole shape of their brain at a glance and choose where to go. */
.brain__section--folded {
  margin-bottom: var(--space-4);
  opacity: 0.72;
}

.brain__section--folded:hover {
  opacity: 1;
}

.brain__section {
  scroll-margin-top: var(--space-12);
}

.brain__blurb {
  max-width: 56ch;
  margin-top: calc(var(--space-4) * -1);
  margin-bottom: var(--space-4);
}

.brain__rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.brain__slot {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.brain__row {
  padding: var(--space-4);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-card);
}

.brain__rowhead {
  margin-bottom: var(--space-3);
}

.brain__fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.brain__field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.brain__field input,
.brain__field textarea,
.brain__field select {
  width: 100%;
}

.brain__field select {
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 1rem;
}

.brain__label {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}

.brain__ro {
  margin: 0;
  color: var(--text-primary);
  white-space: pre-wrap;
}

/* The learned fact being quoted back inside the "stop using this" dialog. It is
   the client's own words about their business, so it is set apart rather than
   run into the sentence around it — and `white-space: pre-wrap` because a fact
   can carry line breaks the store preserved. */
.brain__quoted {
  margin: 0 0 0.75rem;
  padding: 0.5rem 0 0.5rem 0.85rem;
  border-left: 3px solid var(--line-strong);
  color: var(--text-primary);
  white-space: pre-wrap;
}

.brain__help {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin: 0;
  color: var(--text-muted);
  font-size: 0.85rem;
}

.brain__count {
  flex: 0 0 auto;
  color: var(--text-muted);
}

/* The per-field "Read by …" attribution under a How-you-sound row. Rides
   .brain__help for the muted body, plus a nudge inward so it reads as a
   footnote OF the row above it rather than a stray line between rows. */
.brain__readby {
  justify-content: flex-start;
  padding-left: var(--space-2);
  font-style: italic;
}

/* Over the cap. The engine would silently slice the overflow away, so the
   counter turning is the client's only warning before a refused save. */
.brain__count--over {
  color: var(--danger-text);
  font-weight: 700;
}

/* §8.6: where a change applies, said BEFORE the save rather than after it. */
.brain__applies {
  margin: var(--space-3) 0 0;
  color: var(--text-primary);
  font-size: 0.9rem;
}

.brain__rowfoot {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.brain__actions {
  display: flex;
  gap: var(--space-2);
}

/* `[hidden]` is a UA rule at the lowest specificity, so ANY class that sets
   `display` silently defeats it. `.brain__actions { display: flex }` did exactly
   that, and the result was Save and Cancel showing on every row of a form where
   nothing had been touched — a screen that looks like it has thirty unsaved
   changes the moment it opens. Scoped to this surface rather than made global:
   the fix belongs where the pattern is used, not in a rule that changes how
   every other screen in the shell hides things. */
.brain [hidden] {
  display: none !important;
}

.brain__remove {
  margin-left: auto;
}

.brain__rowstatus {
  margin-top: var(--space-2);
}

.brain__add {
  align-self: flex-start;
}

.brain__chip {
  vertical-align: middle;
}

/* ── The learned layer ───────────────────────────────────────────────────── */

.brain__learned {
  padding: var(--space-3) var(--space-4);
  border: 1px dashed var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--wash-learned);
}

.brain__learned .eyebrow {
  margin-bottom: var(--space-1);
}

.brain__learned-value {
  margin: 0;
  color: var(--text-primary);
  font-size: 1.05rem;
}

.brain__learned-note {
  margin: var(--space-1) 0 0;
  color: var(--text-muted);
  font-size: 0.85rem;
}

/* "This isn't right — stop using it". Deliberately quiet: it sits beside every
   learned fact, and a row of loud buttons would read as a list of problems
   rather than a list of things the AI knows. It earns emphasis on hover, when
   the client has already decided to reach for it. */
.brain__suppress {
  display: block;
  margin: var(--space-1) 0 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.82rem;
  color: var(--text-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.brain__suppress:hover,
.brain__suppress:focus-visible {
  color: var(--text-primary);
}

/* The case a client must never misread as "my change didn't take". */
.brain__learned--beaten {
  border-style: solid;
  border-color: var(--line-hairline);
  background: transparent;
}

.brain__learned-line {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}

.brain__found {
  margin-top: var(--space-6);
  padding: var(--space-4);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
}

.brain__foundlist {
  margin: 0 0 var(--space-3);
  padding-left: var(--space-4);
  color: var(--text-primary);
}

.brain__foundlist li {
  margin-bottom: var(--space-2);
}

/* ── Foot ────────────────────────────────────────────────────────────────── */

.brain__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  position: sticky;
  bottom: 0;
  padding: var(--space-3) 0 calc(var(--space-3) + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line-hairline);
  background: var(--glass-navy);
  backdrop-filter: blur(8px);
}

.brain__status {
  margin: 0;
}

.brain__ask {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  color: var(--accent-teal);
}

.core,
.hello,
.section {
  animation: rise 220ms ease-out both;
}

.hello { animation-delay: 40ms; }
.section { animation-delay: 80ms; }

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

/* ── The blog ────────────────────────────────────────────────────────────────
   Tokens only: every colour here is a var, because a hex written into a rule is
   a hex the contrast gate cannot see. Layout follows the shell's existing card
   language rather than inventing a second one — a client should not be able to
   tell that this surface was built after the others. */

.blog {
  display: grid;
  gap: var(--space-6);
}

.blog__h {
  margin: 0;
}

.blog__banner {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line-gold);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  color: var(--text-primary);
}

.blog__status {
  margin: 0;
  min-height: 1.25rem;
  color: var(--text-muted);
}
.blog__status--ok { color: var(--success-text); }
.blog__status--error { color: var(--danger-text); }

/* Tabs. A row that wraps rather than scrolls: four labels fit a phone, and a
   horizontal scroller hides the fourth from anyone who does not think to swipe. */
.blog__tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.blog__tab {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: 0 var(--space-4);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-pill);
  background: var(--surface-card);
  color: var(--text-primary);
  font: inherit;
  cursor: pointer;
}

.blog__tab--on {
  border-color: var(--line-teal);
  background: var(--surface-raised);
  color: var(--text-strong);
}

.blog__count {
  min-width: 1.5rem;
  padding: 0 var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--surface-action);
  color: var(--text-strong);
  text-align: center;
}

.blog__list {
  display: grid;
  gap: var(--space-4);
}

.blog__card {
  padding: var(--space-4);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-card);
}

.blog__kicker {
  margin: 0 0 var(--space-2);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.75rem;
}

.blog__card-h {
  margin: 0 0 var(--space-2);
  color: var(--text-strong);
}

.blog__meta {
  margin: var(--space-2) 0 0;
  color: var(--text-muted);
}

.blog__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.blog__actions button {
  min-height: var(--tap-min);
}

.blog__empty {
  padding: var(--space-6) var(--space-4);
  border: 1px dashed var(--line-hairline);
  border-radius: var(--radius-md);
  text-align: center;
}

.blog__empty-h {
  margin: 0 0 var(--space-2);
  color: var(--text-strong);
}

.blog__cols {
  display: grid;
  gap: var(--space-6);
}

.blog__section-h {
  margin: var(--space-6) 0 var(--space-3);
  color: var(--text-strong);
}

.blog__rows {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--space-2);
}

.blog__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-card);
}

.blog__rowbtn {
  display: grid;
  gap: 2px;
  flex: 1;
  min-height: var(--tap-min);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.blog__rowtitle {
  color: var(--text-strong);
}

.blog__tag {
  padding: 2px var(--space-3);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-pill);
  color: var(--text-muted);
  white-space: nowrap;
  font-size: 0.8125rem;
}
.blog__tag--live {
  border-color: var(--line-teal);
  color: var(--success-text);
}

.blog__group { margin-bottom: var(--space-6); }

.blog__form {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
}

.blog__form button { min-height: var(--tap-min); }

.blog__rule {
  width: 100%;
  margin: var(--space-4) 0 var(--space-2);
  border: 0;
  border-top: 1px solid var(--line-hairline);
}

.blog__more > summary {
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  cursor: pointer;
  color: var(--text-primary);
}

.blog__note {
  padding: var(--space-3);
  border-left: 2px solid var(--line-gold);
  color: var(--text-primary);
}

/* The day one scheduled post goes out, changed in place from "Coming up".
   Sized to sit in a row beside the title rather than to fill the column — and
   never below the tap minimum, because on a phone this IS the reorder control. */
.blog__inlinedate {
  width: auto;
  min-height: var(--tap-min);
  margin: 0;
}

/* The editor. */

.blog__editor {
  display: grid;
  gap: var(--space-4);
}

.blog__back {
  justify-self: start;
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-pill);
  background: var(--surface-card);
  color: var(--text-primary);
  font: inherit;
  cursor: pointer;
}

/* Fields. The same shape .brain__field takes: a flex column so the label sits
   above its control, and a control that fills the width — without which the
   label and the input flow inline and the editor reads as a jumble. */
.blog__label {
  display: block;
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}

.blog__input {
  display: block;
  width: 100%;
  margin-bottom: var(--space-3);
}

select.blog__input {
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 1rem;
}

.blog__block textarea,
.blog__block input {
  display: block;
  width: 100%;
}

.blog__title {
  color: var(--text-strong);
  font-size: 1.25rem;
}

.blog__blocks {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--space-3);
}

.blog__block {
  padding: var(--space-3);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-card);
}

.blog__blockbar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.blog__blocktype {
  flex: 1;
  color: var(--text-muted);
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.blog__blockmove,
.blog__blockdel {
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--text-primary);
  font: inherit;
  cursor: pointer;
}

.blog__blockmove[disabled] {
  color: var(--text-muted);
  cursor: default;
}

.blog__add {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.blog__add button {
  min-height: var(--tap-min);
  padding: 0 var(--space-3);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-pill);
  background: var(--surface-card);
  color: var(--text-primary);
  font: inherit;
  cursor: pointer;
}

.blog__photo { display: grid; gap: var(--space-2); }

.blog__photoimg {
  width: 100%;
  max-width: 22rem;
  height: auto;
  border-radius: var(--radius-md);
}

.blog__read {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-card);
}

.blog__read blockquote {
  margin: 0;
  padding-left: var(--space-4);
  border-left: 2px solid var(--line-teal);
  color: var(--text-primary);
}

/* Two columns once there is room for the form to sit beside the list. */
@media (min-width: 64rem) {
  .blog__cols {
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
    align-items: start;
  }
}

/* "For you" is a door, not an announcement — the link needs to look like one. */
.tasks__open {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  margin-top: var(--space-3);
  padding: 0 var(--space-4);
  border-radius: var(--radius-pill);
  text-decoration: none;
}

/* ── The shared chat window's palette ──────────────────────────────────────
   `chat-window.js` is a byte-identical twin of the CRM's copy (see
   tools/learning-contract.json) and is theme-agnostic on purpose: it draws its
   chrome from seven generic names and lets each host decide what they mean.
   These are the cockpit's answers. Aliases only — no new colour is decided
   here, which is what keeps test/cockpit-contrast.test.ts's "no hex outside
   :root" rule true while a vendored file stays untouched.
   ------------------------------------------------------------------------- */

:root {
  --surface: var(--surface-card);
  --line: var(--line-hairline);
  --ink: var(--text-primary);
  --muted: var(--text-muted);
  --brand: var(--accent-teal);
  --hover: var(--wash-teal);
  --shadow: var(--glow-teal);
}

/* ── Ask for help ──────────────────────────────────────────────────────────
   A chat window, not a document. The launcher is position:fixed and lives
   outside #root so it cannot join the flex column the studio turns #root into
   and steal height from the client's own site; its host is hidden entirely
   while the studio is full screen (see concierge.js `sync()`).

   Every colour here is an existing token. The stylesheet's rule is that no hex
   is decided outside :root, and test/cockpit-contrast.test.ts enforces it.
   ------------------------------------------------------------------------- */

#concierge-host {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 25;
}

#concierge-host[hidden] { display: none; }

.chat__launch {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  /* 48px floor: a tap target on a phone, per the accessibility bar. */
  min-height: 48px;
  padding: 0 1.1rem;
  border: 1px solid var(--line-teal);
  border-radius: 999px;
  background: var(--surface-action);
  color: var(--text-strong);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 6px 20px var(--glow-teal);
}

.chat__launch:hover { background: var(--accent-teal); color: var(--surface-page); }

/* The window.
   ---------------------------------------------------------------------------
   THE SHAPE HAS TO BE FIXED BEFORE THE CONTENT ARRIVES. `chat-window.js` pins
   the corner by reading `offsetWidth`/`offsetHeight` ONCE, at attach time, and
   then writes `left`/`top` with `right`/`bottom: auto`. Give it a box whose
   height still depends on its contents and it measures a nearly-empty panel,
   pins the top edge near the bottom of the screen, and the window then grows
   downward out of the viewport as the thread renders. That is exactly what
   shipped: it opened off screen.

   So the popup gets a DEFINITE `height`, not `max-height`, and the body scrolls
   inside it — the same shape crm/public/concierge.js uses for the same module,
   which is the reference this should have followed the first time.

   Layout is keyed off the classes the module toggles:
     (no .cw-popup) — it never loaded. Fail-open to a bottom sheet.
     .cw-popup      — the corner window; left/top are the module's to own.
     .cw-full       — maximised.
   ------------------------------------------------------------------------- */

.chat__win {
  position: fixed;
  z-index: 26;
  display: flex;
  flex-direction: column;
  background: var(--glass-card);
  border: 1px solid var(--line-teal);
  box-shadow: 0 18px 48px var(--scrim);
  /* Fail-open shape: chat-window.js did not load, so nothing will ever set
     left/top. A bottom sheet needs no positioning help and cannot land off
     screen. */
  left: 0;
  right: 0;
  bottom: 0;
  max-height: min(34rem, calc(100vh - 4rem));
  border-radius: 0.9rem 0.9rem 0 0;
}

/* The corner window. A DEFINITE height — see the note above. */
.chat__win.cw-popup {
  left: auto;
  right: 1rem;
  bottom: 1rem;
  width: min(26rem, calc(100vw - 2rem));
  height: min(34rem, calc(100vh - 6rem));
  max-height: none;
  border-radius: 0.9rem;
  overflow: hidden;
}

/* Maximised. The module clears every inline offset for this mode, so the rule
   only has to describe the shape. */
.chat__win.cw-full {
  inset: 1rem;
  width: auto;
  height: auto;
  max-height: none;
  border-radius: 0.9rem;
  overflow: hidden;
}

/* Phones never get a floating window — the module gates drag and pin off small
   viewports, and this keeps the shape right whatever class is on the element. */
@media (max-width: 760px) {
  .chat__win,
  .chat__win.cw-popup,
  .chat__win.cw-full {
    inset: auto 0 0 0;
    width: auto;
    height: auto;
    max-height: min(80vh, 34rem);
    border-radius: 0.9rem 0.9rem 0 0;
  }
}

.chat__header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: 0 0 auto;
  padding: 0.6rem 0.75rem;
  border-bottom: 1px solid var(--line-hairline);
}

.chat__body {
  display: grid;
  gap: 0.75rem;
  align-content: start;
  padding: 0.85rem;
  /* min-height:0 is what actually lets this scroll inside a flex column. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

.chat__close {
  min-width: 44px;
  min-height: 44px;
  margin-left: auto;
  border: 0;
  border-radius: 0.5rem;
  background: transparent;
  color: var(--text-muted);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
}

.chat__close:hover { background: var(--wash-teal); color: var(--text-strong); }

.chat__title { margin: 0; color: var(--text-strong); font-size: 1rem; }

/* Always rendered, never model-written: this is the AI disclosure. */
.chat__disclosure {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}

/* The thread scrolls; the composer under it does not move. */
.chat__thread {
  display: grid;
  gap: 0.6rem;
  padding-right: 0.25rem;
}

.chat__msg {
  max-width: 85%;
  padding: 0.65rem 0.85rem;
  border-radius: 0.85rem;
}

.chat__msg--you {
  justify-self: end;
  border: 1px solid var(--line-teal);
  background: var(--wash-teal);
}

.chat__msg--guide {
  justify-self: start;
  border: 1px solid var(--line-hairline);
  background: var(--surface-card);
}

/* Model output arrives as text nodes, so long words must wrap rather than
   push the window sideways. */
.chat__text {
  margin: 0;
  color: var(--text-primary);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.chat__thinking { margin: 0; color: var(--text-muted); }

.chat__go { margin-top: 0.6rem; }

.chat__hatch {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 44px;
  color: var(--accent-teal);
}

/* The empty state: an invitation, not an index. */
.chat__opener { display: grid; gap: 0.6rem; }

.chat__openerline { margin: 0; color: var(--text-primary); }

.chat__chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.chat__chip {
  min-height: 44px;
  padding: 0 0.8rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface-inset);
  color: var(--text-primary);
  font: inherit;
  font-size: 0.9rem;
  text-align: left;
  cursor: pointer;
}

.chat__chip:hover { border-color: var(--line-teal); background: var(--wash-teal); }

.chat__composer { display: flex; gap: 0.5rem; }

.chat__input {
  flex: 1 1 auto;
  min-height: 48px;
  padding: 0 0.85rem;
  border: 1px solid var(--line-strong);
  border-radius: 0.6rem;
  background: var(--surface-inset);
  color: var(--text-primary);
  font: inherit;
}

.chat__input::placeholder { color: var(--text-muted); }

/* Staff view only: the grounding, shown instead of a chat it may not use. */
.chat__grounding { display: grid; gap: 0.7rem; }

.chat__gh { margin: 0 0 0.35rem; color: var(--text-strong); font-size: 0.98rem; }

.chat__card {
  padding: 0.85rem;
  border: 1px solid var(--line-hairline);
  border-radius: 0.7rem;
  background: var(--surface-card);
}

.chat__list { margin: 0.4rem 0 0; padding-left: 1.1rem; color: var(--text-primary); }

.chat__list li { margin-bottom: 0.25rem; }

.chat__list--cant { color: var(--text-muted); }

/* ── Setup: what we still need from the client ─────────────────────────────
   Home's "To finish setting up" section (COCKPIT_SETUP_STATUS_LIVE). Each row
   is one thing the CLIENT owes us, read from crm_provisioning_ledger — the
   same rows the team acts on.

   These classes shipped with NO rules at all: the section rendered as a bare
   bulleted list in the middle of a designed dashboard. It was invisible to
   every gate, because `check:ui` only parses the JS and no test asserts a
   class exists in the stylesheet — the shape of defect only a human eye or a
   grep for the class names catches.

   Card-shaped like .vslot rather than a bare list, deliberately: these are the
   only items on Home that ask the reader to go and DO something, so each has
   to read as its own task rather than as prose they can skim. The instruction
   underneath is muted so the ask itself carries the weight. */
.setup__list {
  display: grid;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

.setup__item {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
}

.setup__ask {
  margin: 0;
  font-weight: 600;
  color: var(--text-primary);
}

.setup__how {
  margin: var(--space-1) 0 0;
  font-size: 0.9rem;
}

/* Sits apart from the list on purpose: it is about OUR side, and it must not
   read as a sixth thing the client has to do. */
.setup__ours {
  margin: var(--space-3) 0 0;
  font-size: 0.9rem;
}
