/* The Sign Studio - marketing site v2 (1c editorial hero + section pages).
   Page styles on top of nocturne.css. No external requests, no JS, no cookies. */

/* -- self-hosted Inter (rsms/inter v4.1, web/ hinted woff2) -- */
@font-face {
  font-family: "Inter";
  src: url("../fonts/Inter-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../fonts/Inter-Medium.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Inter";
  src: url("../fonts/Inter-SemiBold.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
/* The product's OWN face. studio_products/doorplate_300x120.json freezes
   "Montserrat ExtraBold" as the door plate font, so the plates drawn on this
   site are lettered in the same face the printer lays down - subset to the
   characters used here (scratchpad build_site_assets.py). */
@font-face {
  font-family: "Plate";
  src: url("../fonts/Montserrat-ExtraBold-subset.woff") format("woff");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}

/* -- brand accent, layered on top of Nocturne (copper = identity) -- */
:root {
  --brand: #CA834D;
  --brand-soft: #E0A878;
  --brand-line: #7A5030;
  --gutter: 48px;
}
@media (max-width: 700px) { :root { --gutter: 24px; } }
@media (max-width: 400px) { :root { --gutter: 20px; } }

.btn-brand { border-color: var(--brand); color: var(--brand-soft); }
.btn-brand:hover { background: rgba(202, 131, 77, .12); }
.btn-brand:active { background: rgba(202, 131, 77, .2); }

/* -- page frame -- */
.wrap { max-width: 1200px; margin: 0 auto; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 10;
  background: var(--color-surface); color: var(--color-text);
  padding: 10px 16px; border-radius: var(--radius-md); font-size: 13px;
}
.skip-link:focus { left: var(--gutter); top: 10px; }

/* Fading rule: freestanding rules fade at both ends (Nocturne signature). */
.rule {
  height: 1px; border: 0; margin: 0;
  background: linear-gradient(90deg, transparent,
    var(--color-divider) 48px, var(--color-divider) calc(100% - 48px), transparent);
}

/* -- nav -- */
/* -- trading notice (TEMPORARY - remove at launch) -- ------------------------
   Sits above the header in the brand copper rather than a hazard yellow: the
   point is to be read, not to look like a building site. Full-bleed band, and
   the inner column repeats .wrap's 1200px + gutter so the text starts exactly
   under the wordmark. */
.notice {
  background: rgba(202, 131, 77, .10);
  border-bottom: 1px solid var(--brand-line);
}
.notice-inner {
  max-width: 1200px; margin: 0 auto; padding: 11px var(--gutter);
  font-size: 12.5px; line-height: 1.55; color: var(--color-neutral-300);
}
.notice strong { color: var(--brand-soft); font-weight: 600; }
.notice a { color: var(--brand-soft); text-underline-offset: 3px; }
.notice a:hover { color: var(--color-text); }

.nav-bar {
  display: flex; align-items: center; gap: 26px;
  padding: 20px var(--gutter); flex-wrap: wrap;
}
.brand {
  display: inline-flex; align-items: center; gap: 10px;
  text-decoration: none; color: var(--color-text);
}
/* The real vector mark (assets/brand/mark_dark.svg) carries its own tile +
   colours - the CSS just sizes it. */
.brand-mark { width: 26px; height: 26px; flex: none; display: block; }
.brand-name { font-size: 13.5px; font-weight: 600; letter-spacing: .02em; }
.nav-links { display: flex; align-items: center; gap: 22px; margin-left: auto; flex-wrap: wrap; }
.nav-links a { font-size: 12.5px; color: var(--color-neutral-400); text-decoration: none; }
.nav-links a:hover { color: var(--color-text); }
.nav-links a[aria-current="page"] { color: var(--color-text); }
.nav-cta { font-size: 12px; padding: 7px 15px; }
@media (max-width: 820px) {
  .nav-bar { gap: 14px; padding-block: 16px; }
  .nav-cta { margin-left: auto; }
  .nav-links { margin-left: 0; width: 100%; gap: 12px 18px; order: 3; }
}

/* -- hero (1c editorial) -- */
.hero {
  display: grid; grid-template-columns: 1fr 452px; gap: 56px;
  padding: 74px var(--gutter) 60px; align-items: center;
}
.kicker {
  font-size: 10.5px; letter-spacing: .15em; text-transform: uppercase;
  color: var(--brand-soft); margin-bottom: 20px;
}
.hero h1 {
  font-size: clamp(34px, 6vw, 62px); line-height: 1.04; letter-spacing: -.025em;
  margin: 0 0 22px; font-weight: 500; text-wrap: balance; max-width: 12ch;
}
.hero-body {
  font-size: 16px; line-height: 1.6; color: var(--color-neutral-400);
  margin: 0 0 30px; max-width: 490px; text-wrap: pretty;
}
/* flex-start, not the default stretch: the room-list box (A3) is a tall cell in
   this row and "See it work" beside it must stay a button, not a full-height
   one. With two buttons in the row it looks exactly as it always did. */
.hero-ctas { display: flex; gap: 12px; margin-bottom: 30px; flex-wrap: wrap; align-items: flex-start; }
.hero-ctas .btn { font-size: 13px; padding: 11px 22px; }
.reassure {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 10px 26px;
  font-size: 11.5px; color: var(--color-neutral-500);
}
.reassure li { display: flex; align-items: center; gap: 8px; }
.reassure li::before {
  content: ""; width: 4px; height: 4px; border-radius: 50%;
  background: var(--brand); flex: none;
}
@media (max-width: 980px) {
  .hero { grid-template-columns: 1fr; gap: 46px; padding-top: 44px; }
  .hero h1 { max-width: none; }
}

/* -- sign plates -------------------------------------------------------------
   A faithful 300 x 120 x 4 mm door plate: the shipped product
   (studio_products/doorplate_300x120.json - rounded_rect, corner 8 mm, four
   countersunk corner fixings, anthracite plate, white Montserrat ExtraBold
   lettering, ref at x40/y100, room name centred at x150/y56).

   ONE input: --w, the plate width in px. Every other number is the real
   millimetre value scaled by --w/300, so the drawing cannot drift from the
   product. Colours come from the real finish palette (sign_generator.COLOR_MAP).
   ------------------------------------------------------------------------- */
.plate {
  --w: 440px;
  --mm: calc(var(--w) / 300);      /* 1 mm of sign = this many px */
  --plate-col: #383E42;            /* anthracite_grey */
  --letter-col: #FFFFFF;           /* white */
  --room-mm: 34;
  position: relative;
  width: var(--w); height: calc(120 * var(--mm));
  border-radius: calc(8 * var(--mm));
  background: linear-gradient(158deg,
    color-mix(in srgb, var(--plate-col) 88%, #ffffff) 0%,
    var(--plate-col) 46%,
    color-mix(in srgb, var(--plate-col) 90%, #000000) 100%);
  box-shadow:
    inset 0 2px 0 rgba(255, 255, 255, .06),
    inset 0 -2px 0 rgba(0, 0, 0, .35),
    0 34px 70px rgba(0, 0, 0, .6);
  font-family: "Plate", "Inter", system-ui, sans-serif;
  font-weight: 800;
  color: var(--letter-col);
  flex: none;
}
/* Four countersunk corner fixings, 10 mm in from each edge. */
.plate::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  background:
    radial-gradient(circle calc(2.4 * var(--mm)) at calc(10 * var(--mm)) calc(10 * var(--mm)), rgba(0,0,0,.55) 60%, transparent 62%),
    radial-gradient(circle calc(2.4 * var(--mm)) at calc(290 * var(--mm)) calc(10 * var(--mm)), rgba(0,0,0,.55) 60%, transparent 62%),
    radial-gradient(circle calc(2.4 * var(--mm)) at calc(10 * var(--mm)) calc(110 * var(--mm)), rgba(0,0,0,.55) 60%, transparent 62%),
    radial-gradient(circle calc(2.4 * var(--mm)) at calc(290 * var(--mm)) calc(110 * var(--mm)), rgba(0,0,0,.55) 60%, transparent 62%);
  pointer-events: none;
}
.plate .plate-ref {
  position: absolute; left: calc(40 * var(--mm)); top: calc(20 * var(--mm));
  transform: translateY(-50%);
  font-size: calc(10 * var(--mm)); letter-spacing: .06em; line-height: 1;
  opacity: .82;
}
.plate .plate-room {
  position: absolute; left: 50%; top: calc(64 * var(--mm));
  transform: translate(-50%, -50%);
  font-size: calc(var(--room-mm) * var(--mm)); line-height: 1;
  letter-spacing: -.01em; white-space: nowrap;
  text-shadow: 0 1px 0 rgba(0, 0, 0, .28);
}
.plate-chalk { --plate-col: #FFFFFF; --letter-col: #383E42; }
.plate-chalk .plate-room { text-shadow: 0 1px 0 rgba(255, 255, 255, .5); }
.plate-blue  { --plate-col: #1E50C8; --letter-col: #FFFFFF; }

/* Hero stack: one family of sizes, right aligned, shadow easing off.

   --w must stay a LENGTH: it drives --mm, which drives height and font-size,
   and a percentage there would resolve against the wrong box (a % in the
   height calc collapses the plate). So the stack is an inline-size container
   and the plate measures itself in cqw - never %, never vw (vw includes the
   scrollbar and overflows narrow columns). Cap per page with --pw-max. */
.plate-stack {
  --pw-max: 440px;
  container-type: inline-size;
  display: flex; flex-direction: column; align-items: flex-end; gap: 26px;
}
/* Position in the stack sizes ANY child: the CSS plate reads --w (it drives the
   mm scale for the type inside it), a rendered plate reads width. Both live here
   - the scroll sequence still needs a plate it can type into. */
.plate-stack > :nth-child(1) { --w: min(var(--pw-max), 100cqw); width: var(--w); }
.plate-stack > :nth-child(2) { --w: min(calc(var(--pw-max) * .773), 77.3cqw); width: var(--w); }
.plate-stack > :nth-child(3) { --w: min(calc(var(--pw-max) * .568), 56.8cqw); width: var(--w); }
.plate-stack > .plate:nth-child(2) { box-shadow:
  inset 0 2px 0 rgba(255,255,255,.06), inset 0 -2px 0 rgba(0,0,0,.35),
  0 22px 46px rgba(0,0,0,.5); }
.plate-stack > .plate:nth-child(3) { box-shadow:
  inset 0 2px 0 rgba(255,255,255,.06), inset 0 -2px 0 rgba(0,0,0,.35),
  0 14px 30px rgba(0,0,0,.42); }

/* A RENDERED plate - scripts/render_site_plates.py, built from the same GLB the
   customer approves. The file is a CUTOUT with alpha and carries no backdrop and
   no baked shadow, which is what lets one asset sit on the dark ground, on the
   section-alt band and in a light band without a seam. The shadow therefore has
   to be drop-shadow: it follows the alpha silhouette, where box-shadow would
   trace the transparent image RECTANGLE and print a hard square corner around a
   round-cornered plate. A cut-out photograph drops into the same rules. */
.plate-img { display: block; height: auto; filter: drop-shadow(0 20px 30px rgba(0,0,0,.55)); }
.plate-stack > .plate-img:nth-child(2) { filter: drop-shadow(0 14px 20px rgba(0,0,0,.46)); }
.plate-stack > .plate-img:nth-child(3) { filter: drop-shadow(0 9px 13px rgba(0,0,0,.4)); }
.plate-caption {
  /* neutral-500, not 600: 600 on this ground is 4.08:1 - the one WCAG AA
     failure on the site (suite review 2026-08-15); 500 measures 5.5:1+ */
  margin: 4px 0 0; font-size: 11px; color: var(--color-neutral-500);
  font-variant-numeric: tabular-nums; text-align: right;
}
@media (max-width: 980px) {
  .plate-stack { align-items: flex-start; }
  .plate-caption { text-align: left; }
}

/* -- the scroll sequence: "one room list -> every door" -------------------
   Two beats. Beat 1 is live DOM (it IS the product interface - crisp at any
   resolution, real selectable text, ~0 bytes). Beat 2 is a real-time WebGL
   corridor, loaded only when the section approaches the viewport.

   The DEFAULT state of this section is the STATIC one: the final corridor
   still, the headline and the button, laid out as an ordinary block. js/hero.js
   adds .is-live only once it has (a) motion permission and (b) a working WebGL
   context, so a JS failure, an old browser, prefers-reduced-motion and a
   404'd payload all land on the same honest fallback rather than a black box.
   ------------------------------------------------------------------------ */
/* Contains the hero's full-bleed track (see .seq.is-live .seq-track below).
   clip, NOT hidden: hidden would make this a scroll container and kill
   position:sticky on the stage, which the whole of beat 2 depends on.
   It must be HERE and not on .seq: .seq sits inside .wrap (max-width 1200px),
   so clipping there clips the very break-out it is meant to contain - the
   track keeps a layout width of 100vw and simply is not VISIBLE past 1200px.
   getBoundingClientRect() reports the LAYOUT box and ignores clipping, so that
   failure measures as a pass. Verify visible width by intersecting rects.
   It is on HTML as well as body because the scrolling element is html: 100vw
   is ~15px wider than the client area once a scrollbar exists, and clipping
   only body still let the DOCUMENT scroll sideways by that much. */
html, body { overflow-x: clip; }

.seq { border-top: 1px solid var(--color-neutral-900); }

/* -- the static fallback (default) -- */
.seq-still-wrap { padding: 40px var(--gutter) 52px; }
.seq-still {
  display: block; width: 100%; height: auto; max-width: 1104px;
  border-radius: var(--radius-lg); background: #0A0C13;
  aspect-ratio: 1200 / 630;
}
.seq-still-copy { max-width: 620px; margin: 26px 0 0; }
.seq-still-copy h2 {
  font-size: clamp(23px, 3vw, 32px); font-weight: 500; letter-spacing: -.02em;
  line-height: 1.14; margin: 0 0 10px; text-wrap: balance;
}
.seq-still-copy p { margin: 0 0 20px; font-size: 14.5px; line-height: 1.6; color: var(--color-neutral-400); }
.seq-still-copy .btn { font-size: 13px; padding: 11px 22px; }
.seq-track { display: none; }
.seq.is-live .seq-still-wrap { display: none; }
.seq.is-live .seq-track { display: block; height: 350vh;
  /* Full-bleed. The section is 1200px inside a 1920px viewport, so a 16:9 film
     under object-fit: cover lost ~37% of its width - which is precisely the
     ends of the sign. At viewport width the stage aspect matches the film. */
  width: 100vw; margin-left: calc(50% - 50vw); }

/* -- the pinned stage -- */
.seq-stage {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
  background: #0A0C13; display: grid;
  /* Pin the row to the stage. A <video> carries an INTRINSIC 16:9 size, so on
     a full-bleed stage it reports a natural height (2545px wide -> ~1432px)
     that stretches an auto-sized row past 100vh. Everything in the cell then
     overflows and the hold copy is clipped by the overflow:hidden above.
     The old <canvas> never did this - its intrinsic size came from its
     drawing-buffer attributes, which three.js kept equal to the CSS box. */
  grid-template-rows: 1fr;
}
/* every layer occupies the same cell, so nothing is positioned absolutely
   against a box whose size we would then have to keep in sync */
/* min-height:0 so a replaced element cannot push the row open from inside:
   a grid item's default min-height is auto, which resolves to intrinsic size. */
.seq-stage > * { grid-area: 1 / 1; min-height: 0; min-width: 0; }

.seq-canvas { width: 100%; height: 100%; display: block; opacity: 0;
  /* CONTAIN, not cover. The film is 16:9 and a browser window almost never is
     - chrome, bookmarks and a non-16:9 monitor all change the stage aspect,
     and cover pays for that by cropping, which cuts the ENDS OFF THE SIGN.
     The sign is the product; it is never allowed to be cropped horizontally.
     contain is the safe default: on a window NARROWER than 16:9 it fits the
     width and bars the top and bottom, which reads as cinematic framing.
     Side bars, by contrast, read as broken - so on a window at least 16:9
     the rule below switches to cover, which then crops only top and bottom.
     Net effect: the ends of the sign survive every window shape, and no
     layout ever shows a vertical bar. */
  object-fit: contain; }
/* wider than the film: cover can only eat top and bottom, so take it */
@media (min-aspect-ratio: 16/9) { .seq-canvas { object-fit: cover; } }
/* The PORTRAIT film (9:16) on a portrait phone. contain would bar 73px top and
   bottom on a 375x812 handset - 18% of the screen - so cover is worth having,
   but only where it provably misses the sign. MEASURED on the delivered film:
   the plate's tightest margin is 13.1% of frame width, and cover crops
   (1 - stageAspect/0.5625)/2 per side: 8.9% at 375x812, 9.0% at 393x852,
   10.0% at 360x800 - but 15.3% on a 344x882, which would cut it. The
   min-aspect-ratio floor of 44/100 keeps ~2% headroom over that boundary; a
   phone taller than it falls back to contain and gets bars instead of a
   cropped sign. The width/orientation pair is the same predicate portraitPhone()
   uses to choose the source, so this only ever applies to the 9:16 film. */
@media (max-width: 760px) and (orientation: portrait) and (min-aspect-ratio: 44/100) {
  .seq-canvas { object-fit: cover; }
}

/* -- beat 1: the design bench -- */
.seq-design {
  display: grid; grid-template-columns: 380px 1fr; gap: 48px;
  align-items: center; justify-items: center;
  /* Beat 1 was composed for a 1200px stage. The stage is full-bleed now for
     the FILM's sake, but this beat must not stretch with it: at 2545px the
     1fr column is ~2000px, which parks the schedule card on the far left and
     drags the hairlines halfway across a 32" monitor. Same content-column
     box as .seq-hold - the layer still spans the stage, its CONTENT does not. */
  padding: 0 max(var(--gutter), calc(50% - 600px)); position: relative;
}
.seq-design .schedule-card { width: 100%; max-width: 380px; }
.seq-design .schedule-table tr { transition: none; }
.seq-design .schedule-table tbody tr { opacity: 0; }
.seq-design .schedule-table tr.is-pick td { background: rgba(202, 131, 77, .13); }
.seq-design .schedule-table tr.is-pick .ref,
.seq-design .schedule-table tr.is-pick td:nth-child(2) { color: var(--brand-soft); }

/* the plate bench. container-type + cqw is what keeps --w a LENGTH: a
   percentage in --w collapses the plate to zero height (it drives --mm, which
   drives the height calc). */
.seq-bench {
  container-type: inline-size;
  width: 100%; max-width: 520px;
  display: flex; flex-direction: column; align-items: center; gap: 18px;
}
.seq-bench .plate { --w: min(440px, 92cqw); }
.seq-swatches { display: flex; gap: 10px; }
.seq-swatch {
  width: 26px; height: 26px; border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .16);
}
.seq-swatch.is-on { box-shadow: 0 0 0 2px var(--brand-soft); }
.seq-dim {
  display: flex; align-items: center; gap: 10px; width: 100%;
  font-size: 11px; color: var(--color-neutral-500);
  font-variant-numeric: tabular-nums; opacity: 0;
}
.seq-dim-bar { height: 1px; background: var(--color-neutral-700); flex: 1; position: relative; }
.seq-dim-bar::before, .seq-dim-bar::after {
  content: ""; position: absolute; top: -4px; width: 1px; height: 9px;
  background: var(--color-neutral-700);
}
.seq-dim-bar::before { left: 0; } .seq-dim-bar::after { right: 0; }

/* the two hairlines from the picked row to the plate. Both ends are measured
   off live rects every frame the layout can have changed - never hardcoded. */
.seq-wires { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.seq-wires path {
  fill: none; stroke: var(--brand); stroke-width: 1; opacity: .8;
  stroke-dasharray: var(--len) var(--len); stroke-dashoffset: var(--len);
}

/* -- the hold: real DOM text over the last frame -- */
.seq-hold {
  display: flex; flex-direction: column; justify-content: flex-end;
  /* The stage is full-bleed now, so a bare gutter puts this hard against the
     VIEWPORT edge instead of the content column - on a wide monitor the copy
     ends up detached from every other block on the page. max() keeps the
     element full width (the scrim gradient must span the whole frame) while
     its CONTENT sits in the same 1200px column .wrap uses. */
  padding: 0 max(var(--gutter), calc(50% - 600px)) clamp(24px, 4vh, 48px);
  opacity: 0; pointer-events: none;
  background: linear-gradient(180deg, transparent 34%, rgba(10, 12, 19, .72) 100%);
}
.seq-hold h2 {
  font-size: clamp(26px, 4.4vw, 46px); font-weight: 500; letter-spacing: -.025em;
  line-height: 1.06; margin: 0 0 20px; max-width: 16ch; text-wrap: balance;
}

@media (max-width: 980px) {
  .seq-design { grid-template-columns: 1fr; gap: 26px; }
  .seq-design .schedule-card { max-width: 460px; }
  .seq.is-live .seq-track { height: 260vh; }
  /* the hairlines are drawn with horizontal control points because they run
     from a table cell ACROSS to the plate. Once the two stack vertically they
     would swoop, so they go rather than lie about the relationship. */
  .seq-wires { display: none; }
}
/* The schedule card USED to be hidden under 760px, on the reasoning that it and
   the plate cannot share a screen at a readable size. That was written when a
   portrait phone could never reach beat 1 at all - it was short-circuited to
   the still - so the rule was never actually looked at. Once phones got their
   own 9:16 film and started running the real sequence, "the plate alone" left
   the beat with no source for the words: the whole point is a room list
   BECOMING a sign, and a phone was seeing only the second half of that.
   They stack instead: list above, plate below, the row highlighting as its
   text fills the plate. The hairlines still go under 980px (stacked, they
   would swoop rather than run across), so the relationship is carried by the
   highlight and the typing rather than by a drawn line. */
@media (max-width: 760px) {
  /* 4vh is only 32px on an 812pt phone, which sits the copy on the screen edge
     - and on an iPhone that edge is where the home indicator lives. The desktop
     value is left exactly as it was; this is a handset floor plus the inset. */
  .seq-hold { padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); }
  .seq-design { gap: 18px; align-content: center; }
  .seq-design .schedule-card { max-width: none; width: 100%; }
  /* the plate drives its own width from --room-mm; give it the screen */
  .seq-bench .plate { --w: min(440px, 96cqw); }
}
/* prefers-reduced-motion never reaches .is-live (js/hero.js checks it first),
   but belt and braces: if it somehow did, the track un-pins. */
@media (prefers-reduced-motion: reduce) {
  .seq.is-live .seq-track { display: none; }
  .seq.is-live .seq-still-wrap { display: block; }
}

/* -- stat band (the ONE saturated field the system permits) -- */
.stats {
  background: linear-gradient(140deg, var(--color-section), var(--color-section-glow));
  padding: 44px var(--gutter);
  /* THREE, matching the three claims in the markup. Was repeat(4) until the
     "48 signs in a typical set" claim was dropped (operator, 2026-08-29). */
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px 24px;
}
/* Full-bleed the band ONLY where there is dead margin to reclaim. A coloured
   band has no reading measure to protect, and a 1200px filled rectangle
   floating in the middle of a 2545px screen reads as a mistake - especially
   now the hero above it spans the viewport. The FILL spans the screen; the
   CONTENT keeps the same column as the prose.
   Below 1200px .wrap is already full width, so the band spans the screen
   anyway and this math would only introduce a scrollbar-width misalignment
   against the prose above it - measured 7px at 885px wide.
   vw and NOT %: percentage padding resolves against the CONTAINING BLOCK
   (.wrap, 1200px), so calc(50% - 600px) collapses to 0 and the content lands
   against the screen edge. .seq-hold can use % only because its containing
   block is the full-bleed stage itself. */
@media (min-width: 1200px) {
  .stats {
    width: 100vw; margin-left: calc(50% - 50vw);
    /* + var(--gutter): the prose column is .wrap's 1200px INSET by the gutter,
       so the text edge is at 50vw - 600px + gutter, not at .wrap's edge.
       Measured at 2545px: prose text lands at 721, .wrap's edge at 673. */
    padding-left: max(var(--gutter), calc(50vw - 600px + var(--gutter)));
    padding-right: max(var(--gutter), calc(50vw - 600px + var(--gutter)));
  }
}
.stat-value {
  font-family: var(--font-heading); font-weight: 500; font-size: 38px;
  line-height: 1.05; letter-spacing: -.02em; font-variant-numeric: tabular-nums;
  margin: 0 0 6px;
}
.stat-label { font-size: 12px; line-height: 1.45; color: var(--color-accent-200); margin: 0; }
@media (max-width: 760px) {
  .stats { grid-template-columns: repeat(2, 1fr); padding-block: 32px; }
  .stat-value { font-size: 30px; }
}
@media (max-width: 400px) { .stats { grid-template-columns: 1fr; } }

/* -- feature columns -- */
.features {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 0;
  border-top: 1px solid var(--color-neutral-900);
}
.feature { padding: 38px 40px 42px; border-right: 1px solid var(--color-neutral-900); }
.feature:first-child { padding-left: var(--gutter); }
.feature:last-child { padding-right: var(--gutter); border-right: 0; }
.feature-icon { color: var(--brand); margin-bottom: 14px; display: block; }
.feature h2, .feature h3 {
  font-size: 17px; font-weight: 500; margin: 0 0 8px;
  letter-spacing: normal; line-height: 1.3;
}
.feature p { margin: 0; font-size: 13.5px; line-height: 1.62; color: var(--color-neutral-400); }
@media (max-width: 980px) {
  .features { grid-template-columns: 1fr; }
  .feature { border-right: 0; border-top: 1px solid var(--color-neutral-900); padding: 26px var(--gutter) 28px; }
  .feature:first-child { border-top: 0; }
}

/* -- generic section -- */
.section { padding: 66px var(--gutter); }
.section-head { max-width: 620px; margin-bottom: 38px; }
.section-head h2 {
  font-size: clamp(25px, 3.4vw, 34px); font-weight: 500; letter-spacing: -.02em;
  line-height: 1.12; margin: 0 0 12px;
}
.section-head p { margin: 0; font-size: 15px; line-height: 1.62; color: var(--color-neutral-400); }
.section-alt { border-top: 1px solid var(--color-neutral-900); }

/* -- how it works: numbered steps -- */
/* an <ol> for the semantics; the copper 01-04 kickers are the visible count,
   so the browser's own markers are turned off */
.steps {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 28px;
  margin-bottom: 44px; list-style: none; padding: 0;
}
/* two-up when the steps sit beside the schedule card (the media queries below
   are later in the cascade, so narrow screens still collapse it to one) */
.steps-2 { grid-template-columns: repeat(2, 1fr); }
.step { border-top: 1px solid var(--color-neutral-800); padding-top: 16px; }
.step-n {
  font-size: 11px; letter-spacing: .12em; color: var(--brand-soft);
  margin: 0 0 8px; font-variant-numeric: tabular-nums;
}
.step h3 { font-size: 15.5px; font-weight: 500; margin: 0 0 7px; letter-spacing: normal; line-height: 1.3; }
.step p { margin: 0; font-size: 13px; line-height: 1.6; color: var(--color-neutral-400); }
@media (max-width: 860px) { .steps { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .steps { grid-template-columns: 1fr; gap: 22px; } }

/* -- ticks -- */
.ticks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 11px; }
.ticks li {
  display: flex; align-items: flex-start; gap: 10px;
  font-size: 13px; line-height: 1.5; color: var(--color-neutral-300);
}
.ticks svg { flex: none; color: var(--brand-soft); margin-top: 3px; }

/* -- sign-schedule card (the 1d spec-sheet card, kept) -- */
.schedule-scroll { overflow-x: auto; }
.schedule-card {
  border-radius: var(--radius-lg); background: var(--color-surface);
  box-shadow: 0 0 0 1px var(--color-neutral-800), 0 28px 64px rgba(0, 0, 0, .5);
  overflow: hidden; min-width: 320px;
}
.schedule-head { display: flex; align-items: center; gap: 9px; padding: 13px 18px; }
.schedule-head svg { flex: none; color: var(--color-accent-400); }
.schedule-title { font-size: 12.5px; font-weight: 500; flex: 1; }
.schedule-meta {
  font-size: 11px; color: var(--color-neutral-500);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.schedule-card .rule {
  background: linear-gradient(90deg, transparent,
    var(--color-divider) 40px, var(--color-divider) calc(100% - 40px), transparent);
}
.schedule-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.schedule-table th {
  font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--color-neutral-500); font-weight: 600; text-align: left;
  padding: 11px 8px 11px 0;
}
.schedule-table td { padding: 10px 8px 10px 0; border-top: 1px solid var(--color-neutral-900); }
.schedule-table th:first-child, .schedule-table td:first-child { padding-left: 18px; width: 52px; }
.schedule-table th:last-child, .schedule-table td:last-child { padding-right: 18px; width: 78px; text-align: right; }
.schedule-table .col-type { width: 94px; }
.schedule-table .ref { color: var(--color-neutral-500); font-variant-numeric: tabular-nums; }
.schedule-table .type { color: var(--color-neutral-400); font-size: 11.5px; }
.schedule-table .price { font-variant-numeric: tabular-nums; white-space: nowrap; }
.schedule-table tr.hl td { background: var(--color-accent-900); }
.schedule-table tr.hl .ref { color: var(--color-accent-300); }
.schedule-table tr.hl .type { color: var(--color-accent-200); }
.schedule-foot {
  padding: 13px 18px; border-top: 1px solid var(--color-neutral-800);
  display: flex; align-items: center; gap: 12px;
}
.schedule-foot-note { font-size: 11.5px; color: var(--color-neutral-500); flex: 1; }
.schedule-foot .btn { font-size: 11.5px; padding: 6px 12px; white-space: nowrap; }
@media (max-width: 420px) {
  .schedule-table th:first-child, .schedule-table td:first-child { width: 44px; }
  .schedule-table .col-type { width: 78px; }
  .schedule-table th:last-child, .schedule-table td:last-child { width: 66px; }
}

/* two-up: prose beside the schedule card */
.split { display: grid; grid-template-columns: 1fr 452px; gap: 56px; align-items: center; }
@media (max-width: 980px) { .split { grid-template-columns: 1fr; gap: 36px; } }

/* -- page header (section pages) -- */
.page-head { padding: 60px var(--gutter) 44px; max-width: 760px; }
.page-head h1 {
  font-size: clamp(30px, 5vw, 46px); font-weight: 500; line-height: 1.06;
  letter-spacing: -.025em; margin: 0 0 18px; text-wrap: balance;
}
.page-head .lede { font-size: 16px; line-height: 1.62; color: var(--color-neutral-400); margin: 0; }

/* -- prose + card grids -- */
.prose { max-width: 680px; }
.prose h3 { font-size: 17px; font-weight: 500; margin: 30px 0 8px; letter-spacing: normal; }
.prose h3:first-child { margin-top: 0; }
.prose p { margin: 0 0 14px; font-size: 14.5px; line-height: 1.65; color: var(--color-neutral-300); }
.prose ul { margin: 0 0 16px; padding-left: 20px; }
.prose li { font-size: 14.5px; line-height: 1.6; color: var(--color-neutral-300); margin-bottom: 7px; }

.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.cards-2 { grid-template-columns: repeat(2, 1fr); }
.info-card {
  background: var(--color-surface); border-radius: var(--radius-md);
  box-shadow: 0 0 0 1px var(--color-neutral-800);
  padding: 22px 22px 24px;
}
.info-card h3 { font-size: 15.5px; font-weight: 500; margin: 0 0 8px; letter-spacing: normal; line-height: 1.3; }
.info-card p { margin: 0 0 10px; font-size: 13.5px; line-height: 1.6; color: var(--color-neutral-400); }
.info-card p:last-child { margin-bottom: 0; }
.info-card .meta {
  font-size: 11px; color: var(--color-neutral-500); font-variant-numeric: tabular-nums;
}
.card-soon { border-style: dashed; box-shadow: none; border: 1px dashed var(--color-neutral-800); background: transparent; }
@media (max-width: 860px) { .cards, .cards-2 { grid-template-columns: 1fr; } }

/* -- finish swatches -- */
.swatches {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 18px;
}
.swatches li { text-align: center; }
.swatch {
  /* a <span> is inline: width/aspect-ratio are ignored until it is a block */
  display: block; aspect-ratio: 1; width: 100%; max-width: 72px;
  margin-inline: auto; border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .14);
  margin-bottom: 9px;
}
.swatch-name { font-size: 12px; color: var(--color-neutral-300); display: block; }
/* The palette splits: some finishes are offered for the plate AND the lettering,
   some for the lettering only. The engine enforces this split (a plate slot set
   to an accent-only colour is refused outright), so the page has to show it. */
.swatch-group { font-size: 15.5px; font-weight: 500; letter-spacing: normal;
  margin: 0 0 16px; }
.swatch-group + .swatches { margin-bottom: 30px; }
.swatch-note { font-size: 13px; color: var(--color-neutral-500); margin: -6px 0 16px; }

/* -- FAQ: native <details>, no JavaScript -- */
.faq { max-width: 760px; }
.faq details { border-top: 1px solid var(--color-neutral-900); }
.faq details:last-of-type { border-bottom: 1px solid var(--color-neutral-900); }
.faq summary {
  cursor: pointer; list-style: none; padding: 18px 30px 18px 0; position: relative;
  font-family: var(--font-heading); font-weight: 500; font-size: 15.5px; line-height: 1.4;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: ""; position: absolute; right: 6px; top: 25px;
  width: 8px; height: 8px; border-right: 1.5px solid var(--brand-soft);
  border-bottom: 1.5px solid var(--brand-soft); transform: rotate(45deg);
  transition: transform .15s ease;
}
.faq details[open] summary::after { transform: rotate(-135deg); top: 28px; }
.faq summary:hover { color: var(--brand-soft); }
.faq details p {
  margin: 0 0 18px; font-size: 14px; line-height: 1.65;
  color: var(--color-neutral-400); max-width: 62ch;
}

/* -- closing CTA -- */
.cta-band {
  border-top: 1px solid var(--color-neutral-900);
  padding: 56px var(--gutter);
  display: flex; align-items: center; justify-content: space-between;
  gap: 24px 40px; flex-wrap: wrap;
}
.cta-band h2 {
  font-size: clamp(23px, 3vw, 30px); font-weight: 500; margin: 0 0 8px;
  letter-spacing: -.02em; line-height: 1.15;
}
.cta-band p { margin: 0; font-size: 14px; color: var(--color-neutral-400); }
.cta-band .btn { font-size: 13px; padding: 11px 22px; }

/* -- contact -- */
.contact-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 22px; max-width: 520px; }
.contact-list dt, .contact-label {
  font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--color-neutral-500); margin-bottom: 5px;
}
.contact-value { font-size: 16px; }
.contact-value a { color: var(--brand-soft); }
.contact-note { font-size: 13px; color: var(--color-neutral-400); margin-top: 4px; }

/* -- the wall: fifteen real plates, one finish -- ---------------------------
   NOT screenshots. Each tile is the .plate component, so every dimension is the
   real millimetre value scaled by --w/300 and the whole thing stays sharp at any
   size. .plate needs --w as a LENGTH (a % resolves against the wrong box and
   collapses the height), so each cell is its own inline-size container and the
   plate measures itself in cqw - the .plate-stack pattern, reused. */
.wall { padding: 74px var(--gutter) 78px; border-top: 1px solid var(--color-neutral-900); }
.wall-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 30px 60px; flex-wrap: wrap; margin-bottom: 40px;
}
.wall-head h2 {
  font-size: clamp(26px, 3.4vw, 40px); font-weight: 500; letter-spacing: -.02em;
  line-height: 1.1; margin: 0; max-width: 15ch;
}
.wall-note {
  margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--color-neutral-400);
  max-width: 40ch;
}
.wall-grid {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 20px;
}
.wall-cell { container-type: inline-size; }
.wall-cell .plate {
  --w: min(216px, 100cqw);
  /* the wall is a field of signs, not five hero shots - the component's own
     0 34px 70px shadow is far too heavy at this size and turns the grid to soup */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .07),
    inset 0 -1px 0 rgba(0, 0, 0, .3),
    0 10px 20px rgba(0, 0, 0, .45);
  transition: background .45s ease, color .45s ease;
}
/* the finishes are the REAL filament colours, same values as .plate-chalk /
   .plate-blue - one palette, declared once above, echoed here for the group */
.wall-grid.is-chalk .plate { --plate-col: #FFFFFF; --letter-col: #383E42; }
.wall-grid.is-chalk .plate-room { text-shadow: 0 1px 0 rgba(255, 255, 255, .5); }
.wall-grid.is-blue  .plate { --plate-col: #1E50C8; --letter-col: #FFFFFF; }

.wall-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 20px 40px; flex-wrap: wrap; margin-top: 38px;
}
.wall-finishes { display: flex; gap: 10px; flex-wrap: wrap; }
.wall-finish {
  display: inline-flex; align-items: center; gap: 9px;
  font: inherit; font-size: 12.5px; color: var(--color-neutral-400);
  background: transparent; cursor: pointer;
  border: 1px solid var(--color-neutral-800); border-radius: 999px;
  padding: 7px 15px 7px 9px;
}
.wall-finish span {
  width: 15px; height: 15px; border-radius: 50%; background: var(--sw);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .16); flex: none;
}
.wall-finish:hover { color: var(--color-text); border-color: var(--color-neutral-600); }
.wall-finish.is-on { color: var(--color-text); border-color: var(--brand); }
/* the wall proves the designer without showing it; this is the way in */
.wall-see {
  font-size: 13px; color: var(--brand-soft); text-underline-offset: 4px;
  margin-left: 4px;
}
.wall-foot > .btn { margin-left: auto; }
@media (max-width: 980px) { .wall-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 620px) { .wall-grid { grid-template-columns: repeat(2, 1fr); gap: 14px; } }
@media (prefers-reduced-motion: reduce) { .wall-cell .plate { transition: none; } }

/* -- screenshots: the designer, shown -- -----------------------------------
   Assets come from scripts\capture_site_screens.py and are ELEMENT captures,
   not windows: a 1440px app window rendered into a 537px column is unreadable
   grey, which is exactly how the first attempt at this section failed. So the
   sizes below are a contract with that script - a dialog captured at 3x is
   shown around 540px, the one full window is shown full width or not at all.
   A screenshot is a CLAIM: never restage or retouch one, and re-run the
   capture (including after a pricing change) rather than patching a file. */
.shots { padding: 66px var(--gutter) 74px; border-top: 1px solid var(--color-neutral-900); }
.shot-figure { margin: 0; }
.shot {
  display: block; width: 100%; height: auto; border-radius: 10px;
  border: 1px solid var(--color-neutral-800);
  box-shadow: 0 24px 50px rgba(0, 0, 0, .5);
}
.shot-cap {
  /* neutral-500 for the same reason .plate-caption is: 600 fails AA on this ground */
  margin: 12px 0 0; font-size: 12.5px; line-height: 1.6;
  color: var(--color-neutral-500); max-width: 60ch;
}
.shot-cap b { color: var(--color-neutral-300); font-weight: 500; }
.shot-wide + .shot-pair, .shot-pair + .shot-wide,
.shot-wide + .shot-wide, .shot-pair + .shot-aside,
.shot-wide + .shot-aside { margin-top: 56px; }
/* Not 1fr 1fr: the room-list dialog is landscape (1.21) and the apply dialog
   is portrait (0.71), so equal columns leave the left one ending 300px short
   with its caption stranded. Weighting the columns lands them near level. */
.shot-pair { display: grid; grid-template-columns: 1.45fr 1fr; gap: 34px; }
/* the two dialogs differ in height; align their tops, not their boxes */
.shot-pair .shot { align-self: start; }
.shot-aside {
  display: grid; grid-template-columns: 420px 1fr; gap: 48px; align-items: center;
}
.shot-aside h3 {
  font-size: 19px; font-weight: 500; letter-spacing: -.01em; margin: 0 0 12px;
}
.shot-aside p {
  margin: 0 0 14px; font-size: 14px; line-height: 1.65;
  color: var(--color-neutral-400); max-width: 52ch;
}
.shot-aside p:last-child { margin-bottom: 0; }
/* The approval viewer is a narrow centred column in a browser, so it is shown
   as one - NOT as a phone. It was captured on a phone until the operator pointed
   out (2026-08-29) that the link arrives by email and in a business gets opened
   at a desk far more often. It is responsive either way; the site just must not
   claim the phone is the normal case. */
.shot-column { border-radius: 10px; }
@media (max-width: 860px) {
  .shot-pair { grid-template-columns: 1fr; gap: 40px; }
  .shot-aside { grid-template-columns: 1fr; gap: 26px; justify-items: start; }
  .shot-column { max-width: 340px; }
}

/* -- footer -- */
.site-foot { padding: 26px var(--gutter) 34px; }
.site-foot-inner {
  display: flex; align-items: baseline; gap: 10px 26px; flex-wrap: wrap;
  padding-top: 24px;
}
.site-foot-brand { font-size: 12.5px; color: var(--color-neutral-400); }
.site-foot-brand a { color: var(--color-neutral-300); text-underline-offset: 3px; }
.site-foot-brand a:hover { color: var(--color-text); }
.site-foot-privacy { font-size: 11.5px; color: var(--color-neutral-500); margin-left: auto; }
.site-foot-nav { display: flex; flex-wrap: wrap; gap: 8px 20px; padding-top: 18px; }
.site-foot-nav a { font-size: 12px; color: var(--color-neutral-500); text-decoration: none; }
.site-foot-nav a:hover { color: var(--color-text); }
@media (max-width: 700px) {
  .site-foot-privacy { margin-left: 0; }
}

/* -- "Start from your room list" (A3) ----------------------------------------
   js/start.js builds this box into an empty .startbox and hides the plain
   "Start a project" link sitting beneath it; with JavaScript off the box is
   never rendered and the link is the CTA. The inputs are nocturne.css's own
   .input - the site loads that stylesheet already, so there is one look for a
   field across the site and the designer.

   [hidden] needs the !important: .btn sets display:inline-flex, which beats the
   user agent's own [hidden] rule, so the link would stay visible under the box.
   ------------------------------------------------------------------------- */
[hidden] { display: none !important; }

.cta-start { flex: 1 1 340px; min-width: 0; max-width: 460px; }
.startbox {
  background: var(--color-surface);
  border: 1px solid var(--color-neutral-800);
  border-radius: var(--radius-lg);
  padding: 18px 18px 16px;
}
.startbox-title {
  margin: 0 0 12px; font-size: 13px; font-weight: 600;
  color: var(--color-text); letter-spacing: -.01em;
}
.startbox-two { display: flex; gap: 12px; flex-wrap: wrap; }
.startbox-two .startbox-field { flex: 1 1 150px; min-width: 0; }
.startbox-field { display: block; margin-bottom: 12px; }
.startbox-label {
  display: block; margin-bottom: 5px;
  font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--color-neutral-500);
}
.startbox .input { font-size: 13px; }
.startbox-rooms {
  min-height: 96px; resize: vertical;
  font-variant-numeric: tabular-nums; tab-size: 8;
}
.startbox-hint {
  margin: -6px 0 18px; font-size: 11px; line-height: 1.45;
  color: var(--color-neutral-500);
}
.startbox-go {
  width: 100%; justify-content: center; font-size: 13px; padding: 11px 22px;
  margin-top: 2px;
}
.startbox-msg { margin: 12px 0 0; font-size: 12px; line-height: 1.5; color: var(--brand-soft); }
/* The rows an import could not read, reported HERE rather than left behind a
   redirect: the visitor is on another origin, so the designer cannot be told
   about them afterwards (A3). The form is replaced, not disabled - the project
   already exists and a second Start would mint a second one. */
.startbox-done .startbox-title { margin-bottom: 10px; }
.startbox-errs {
  margin: 0 0 12px; padding-left: 18px;
  display: flex; flex-direction: column; gap: 5px;
  font-size: 11.5px; line-height: 1.45; color: var(--color-neutral-300);
}
.startbox-more { margin: 8px 0 0; font-size: 11px; line-height: 1.45; color: var(--color-neutral-500); }
.startbox-done .startbox-go { margin-top: 4px; }
/* the honeypot: off the screen, out of the tab order, and aria-hidden in the
   markup - invisible to a person and to a screen reader alike */
.startbox-hp {
  position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden;
}
@media (max-width: 700px) {
  .cta-start { flex-basis: 100%; max-width: none; }
  .startbox { padding: 16px 16px 14px; }
}
