/* The palette, declared once for both halves of the site.

   The site is served by two builders on purpose — scripts/build-site-html.mjs
   owns / and /examples, website/ owns /docs and /playground; docs/spec/06-
   website.md argues the split. What was never on purpose is that each of them
   also carried its own copy of the palette. Twenty-five tokens were declared
   twice, and by the time they were compared five had drifted:

     --pass       #7a9a5e  ·  #6f8f54     the same badge, two greens
     --body       15px     ·  1rem
     --caption    11px     ·  .72rem
     --headline   px clamp ·  rem clamp
     --display    px clamp ·  rem clamp

   Three of those five had drifted because nobody was looking: --display was
   used by neither file, and --caption and --headline by the static side only.
   A token no page reads cannot be seen to disagree, so it disagrees quietly
   until someone reaches for it. Those live in the stylesheet that reads them;
   only what BOTH surfaces read is declared here.

   Where a token had genuinely to be chosen, the rem-based, higher-contrast
   value won — see the two notes below.

   Loaded by web/site.css (@import, and the build inlines it) and by
   website/app/global.css. Colour for DRAWINGS is a different question with a
   different owner: lib/theme.mjs. */

:root {
  /* Dark ground — the hero stage, the footer, the 3D background. */
  --void: #12161c;
  --void-2: #1b212a;
  --void-line: #2a323d;
  --lit: #f2efe8;
  --lit-dim: #9aa6b4;

  /* Light ground — the page, and the sheet a drawing sits on. */
  --paper: #faf8f3;
  --sheet: #ffffff;
  --ink: #16324f;
  --ink-2: #4a5f78;
  --soft: #8b96a3;
  --rule: #e0dbd0;

  --accent: #c2761c;
  --accent-lit: #e0913a;

  /* Verdict colours. #6f8f54 rather than the gallery's lighter #7a9a5e: both
     surfaces show pass badges, so they have to agree, and this one is the
     darker of the two. It still only reaches 3.45:1 on paper where --fail
     reaches 5.24:1 — under the 4.5:1 AA line. Closing that gap means moving
     the green (roughly to #5c7745), which is a palette decision rather than a
     deduplication, so it is named here and left. */
  --pass: #6f8f54;
  --fail: #b4432b;

  --radius: 2px;
  --shadow: 0 1px 2px rgba(22, 50, 79, 0.05), 0 12px 34px -14px rgba(22, 50, 79, 0.22);

  --space-1: 11px;
  --space-2: 22px;
  --space-3: 44px;
  --space-4: 88px;

  /* 1rem rather than the gallery's 15px. A body size in px ignores the reader's
     own browser font-size setting; every other size on both surfaces is derived
     from this one, so it is the single place that decision is made. The gallery
     and example pages therefore set body text one pixel larger than before. */
  --body: 1rem;
}

/* The palette is web/tokens.css, shared with the Next application. The build
   prepends it to this file — not @import — for the same reason it appends the
   masthead: one stylesheet, one request, and no rule reaching for a colour
   that has not arrived yet. See scripts/build-site-html.mjs.

   What only this half of the site reads. A token the Next application never
   resolves does not belong in the shared file: it cannot be seen to agree
   there, so it would only drift there. */
:root {
  --grid: rgba(22, 50, 79, 0.045);
  --warning: #a76921;
  --headline: clamp(30px, 4vw, 48px);
  --caption: 11px;
  --wrap: 1200px;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background-color: var(--paper);
  background-image:
    linear-gradient(var(--grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid) 1px, transparent 1px);
  background-size: 22px 22px;
  color: var(--ink);
  font: 400 var(--body) / 1.62 "IBM Plex Sans", sans-serif;
  -webkit-font-smoothing: antialiased;
}

body,
button,
input {
  font-family: "IBM Plex Sans", sans-serif;
}

a {
  color: inherit;
}

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

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

.skip-link {
  position: fixed;
  left: 12px;
  top: 12px;
  z-index: 100;
  transform: translateY(-80px);
  background: var(--sheet);
  border: 1px solid var(--rule);
  padding: 8px 12px;
  text-decoration: none;
}

.skip-link:focus {
  transform: none;
}

.wrap {
  width: min(100%, var(--wrap));
  margin: 0 auto;
  padding-inline: 28px;
}

.eyebrow,
.crumb,
.filter-label,
.plate-index,
.card-meta,
.label,
.drag-note,
.footer-note {
  font: 500 var(--caption) / 1.2 "IBM Plex Mono", monospace;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* The masthead lives in web/masthead.css, which the Next application imports
   too — see lib/site-nav.mjs. The build appends it to this file, so nothing
   here needs to know it moved. */

/* ============================================================================
   HOME HERO — one file, asked for, written, assembled, cut into every sheet.
   Runtime in web/home-hero.js. Colour and light come from the theme payload.
   All classes namespaced under .home-hero-* to stay clear of the gallery and
   example pages (which use .hero-title, .eyebrow of their own).
   ========================================================================== */

/* Screen-reader / crawler copy for the hero. The visible headline is short; the
   full sentence lives here so the page is legible without the animation. */
.home-hero-description {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.home-hero {
  --gutter: max(28px, calc((100% - var(--wrap)) / 2 + 28px));
  position: relative;
  height: min(90vh, 860px);
  min-height: 640px;
  overflow: hidden;
  border-bottom: 1px solid var(--rule);
}

/* ---- stage ---- */

.home-hero-stage {
  position: absolute;
  z-index: 1;
  /* Inset on the right so the model never sits under the chrome and never
     bleeds off the viewport. */
  inset: 0 0 64px 0;
}

/* The canvas MUST be positioned. Without it the canvas is a non-positioned
   block, and CSS paints those beneath every positioned sibling — so the
   `z-index: 0` backdrop layer that used to live here was painting ON TOP of the
   model, not behind it. Its opacity was driven by build progress
   (--presence = entities / 122), which made it a milky film that thickened as
   the model completed: invisible at the start, 72% opaque over the centre of
   the frame by the last line of the file. The backdrop is gone — behind an
   opaque WebGL canvas it could never have shown anyway — and the canvas now
   holds the stacking position the layout always assumed it had. */
.home-hero-canvas {
  position: absolute;
  z-index: 1;
  inset: 0;
}

.home-hero-canvas,
.home-hero-canvas canvas { display: block; width: 100%; height: 100%; }

/* A soft paper wash behind the text column only. It stops before the model
   starts, so it backs the headline without milking the building.

   The fade is a smooth, continuous decline — NOT a near-opaque plateau that
   then drops off a cliff. An earlier `.97 → .9 → 0` gradient held ~90% for its
   left two-thirds and then fell fast, and that slope change read as a vertical
   seam where the faint background grid suddenly reappeared. Spreading the fade
   evenly across the whole width removes the perceived edge. */
.home-hero::before {
  position: absolute;
  z-index: 2;
  inset: 0 60% 64px 0;
  background: linear-gradient(90deg,
    rgba(250, 248, 243, .94) 0%,
    rgba(250, 248, 243, .82) 26%,
    rgba(250, 248, 243, .5) 58%,
    rgba(250, 248, 243, .18) 82%,
    rgba(250, 248, 243, 0) 100%);
  content: "";
  pointer-events: none;
}

/* ---- fallback poster (no WebGL, or WebGL init failed) ---- */

.home-hero-poster {
  position: absolute;
  z-index: 1;
  inset: 0;
  opacity: 0;
  pointer-events: none;
}

.home-hero-poster svg {
  display: block;
  width: 100%;
  height: 100%;
  transform: translateX(12%) scale(.94);
  transform-origin: center;
}

.home-hero.is-fallback .home-hero-poster { opacity: 1; }
.home-hero.is-fallback .home-hero-canvas,
.home-hero.is-fallback .home-hero-chrome { display: none; }

.home-hero-enable {
  position: absolute;
  z-index: 9;
  right: var(--gutter);
  bottom: 20px;
  display: none;
  min-height: 36px;
  align-items: center;
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  padding: 0 14px;
  background: rgba(250, 248, 243, .94);
  color: var(--ink);
  cursor: pointer;
  font: 600 9px/1 "IBM Plex Mono", monospace;
  letter-spacing: .1em;
  text-transform: uppercase;
}

.home-hero.is-opt-in .home-hero-enable { display: inline-flex; }
.home-hero-enable:hover { background: var(--ink); color: var(--paper); }
.home-hero-enable:disabled { cursor: wait; opacity: .62; }

/* ---- headline ---- */

.home-hero-title {
  position: absolute;
  z-index: 6;
  top: clamp(52px, 8vh, 92px);
  left: var(--gutter);
  width: min(46ch, 40vw);
}

.home-hero-eyebrow {
  margin: 0 0 14px;
  color: var(--accent);
  font: 600 10px/1 "IBM Plex Mono", monospace;
  letter-spacing: .17em;
  text-transform: uppercase;
}

.home-hero-title h1 {
  margin: 0;
  font: 400 clamp(34px, 4.2vw, 58px)/1.03 "Instrument Serif", serif;
  letter-spacing: -.02em;
  text-wrap: balance;
}

.home-hero-lede {
  margin: 16px 0 0;
  max-width: 40ch;
  color: var(--ink-2);
  font-size: 14.5px;
  line-height: 1.55;
}

/* ---- the sheet panel: the ask, then the source, then the drawings ---- */

.home-hero-panel {
  position: absolute;
  z-index: 7;
  bottom: 82px;
  left: var(--gutter);
  width: 424px;
  overflow: hidden;
  border: 1px solid rgba(22, 50, 79, .18);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, .95);
  box-shadow: var(--shadow);
}

.home-hero-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid var(--rule);
  padding: 0 10px;
  height: 30px;
  color: var(--ink-2);
  background: rgba(250, 248, 243, .96);
  font: 600 9px/1 "IBM Plex Mono", monospace;
  letter-spacing: .11em;
  text-transform: uppercase;
}

.home-hero-panel-head b { color: var(--ink); font-weight: 600; }
.home-hero-panel-head span { color: var(--soft); font-variant-numeric: tabular-nums; }

/* Sized to the drawings' own aspect (they run 1.34–1.57) so a sheet fills the
   box instead of letterboxing inside a 2:1 slot. */
.home-hero-panel-body { position: relative; height: 262px; }

.home-hero-panel-body > * {
  position: absolute;
  inset: 0;
  transition: opacity 260ms ease;
}

/* The ask at full size: alone in the panel, vertically centred. Mono — the same
   family as the file it produces — so the panel reads as one document, not a
   chat bubble. The `›` is a terminal register: a request was issued, output
   follows. */
.home-hero-ask {
  display: flex;
  padding: 16px 16px 14px;
  align-items: center;
  opacity: 0;
  transition: opacity 300ms ease;
}

.home-hero-panel[data-mode="ask"] .home-hero-ask { opacity: 1; }

.home-hero-ask p {
  margin: 0;
  color: var(--ink);
  font: 500 16px/1.6 "IBM Plex Mono", monospace;
  text-wrap: balance;
}

.home-hero-ask p::before {
  margin-right: 9px;
  color: var(--accent);
  content: "›";
}

.home-hero-ask span {
  opacity: 0;
  transition: opacity 260ms ease;
}

.home-hero-ask span.in { opacity: 1; }

/* Demoted: the same sentence, shrunk to a pinned header that stays for the whole
   build — visible for exactly as long as it is being fulfilled. */
.home-hero-ask-line {
  overflow: hidden;
  margin: 0 0 9px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--rule);
  color: var(--ink-2);
  font: 500 9.5px/1.4 "IBM Plex Mono", monospace;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.home-hero-ask-line::before {
  margin-right: 7px;
  color: var(--accent);
  content: "›";
}

.home-hero-src {
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  padding: 12px 12px 10px;
  opacity: 0;
}

.home-hero-panel[data-mode="src"] .home-hero-src { opacity: 1; }

.home-hero-src-stage {
  margin: 0 0 9px;
  color: var(--accent);
  font: 600 9px/1 "IBM Plex Mono", monospace;
  letter-spacing: .13em;
  text-transform: uppercase;
}

.home-hero-src ol {
  display: grid;
  align-content: start;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.home-hero-src li {
  display: grid;
  grid-template-columns: 26px 1fr;
  gap: 8px;
  align-items: baseline;
  color: var(--ink-2);
  font: 500 10.5px/1.45 "IBM Plex Mono", monospace;
}

.home-hero-src li > i {
  color: var(--soft);
  font-size: 8.5px;
  font-style: normal;
  font-variant-numeric: tabular-nums;
}

.home-hero-src li > code {
  overflow: hidden;
  font: inherit;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.home-hero-src li.pending { opacity: .3; }
.home-hero-src li.live { color: var(--accent); }

.home-hero-src-foot {
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--rule);
  color: var(--soft);
  font: 500 9px/1 "IBM Plex Mono", monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: .08em;
}

.home-hero-src-foot b { color: var(--ink); font-weight: 600; }

/* flex with a definite height: a percentage max-height needs a resolvable
   basis, which a grid auto-row does not give — so the drawing letterboxes and
   never spills whatever its aspect turns out to be. */
.home-hero-sheet {
  display: flex;
  margin: 0;
  padding: 9px 11px 11px;
  align-items: center;
  justify-content: center;
  background: var(--sheet);
  opacity: 0;
}

.home-hero-panel[data-mode="sheet"] .home-hero-sheet { opacity: 1; }

.home-hero-sheet img {
  display: block;
  min-width: 0;
  min-height: 0;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
}

/* ---- chrome: one row — controls · narration · one bar · clock ---- */

.home-hero-chrome {
  position: absolute;
  z-index: 9;
  right: var(--gutter);
  bottom: 24px;
  left: var(--gutter);
  display: grid;
  /* Fixed columns so the bar's two edges stay put — a progress bar whose zero
     moves as the caption changes length is worse than none. The clock is a
     fixed 84px because its character count changes (0.5 → 10.7 → 46.0). */
  grid-template-columns: auto minmax(0, 290px) minmax(120px, 1fr) 84px;
  gap: 16px;
  align-items: center;
}

.home-hero-chrome-controls { display: flex; gap: 2px; }

.home-hero-beat {
  overflow: hidden;
  margin: 0;
  font: 500 10px/1 "IBM Plex Mono", monospace;
  letter-spacing: .12em;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
}

.home-hero-beat em {
  color: var(--ink);
  font-style: normal;
  font-weight: 600;
}

.home-hero-beat em::after {
  margin: 0 7px;
  color: var(--soft);
  content: "·";
  font-weight: 400;
}

.home-hero-beat span { color: var(--accent); }

/* The single bar. Track and fill are drawn here; the range input lies on top,
   invisible except its thumb, so dragging works without a second track. */
.home-hero-bar { position: relative; height: 14px; }

.home-hero-bar::before,
.home-hero-bar::after {
  position: absolute;
  top: 6px;
  left: 0;
  height: 2px;
  content: "";
}

.home-hero-bar::before { right: 0; background: rgba(22, 50, 79, .14); }
.home-hero-bar::after { width: calc(var(--t, 0) * 100%); background: var(--accent); }

.home-hero-bar-tick {
  position: absolute;
  top: 2px;
  width: 1px;
  height: 10px;
  background: rgba(22, 50, 79, .26);
}

.home-hero-bar input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  appearance: none;
  background: none;
  cursor: pointer;
}

.home-hero-bar input::-webkit-slider-runnable-track { height: 100%; background: none; }
.home-hero-bar input::-moz-range-track { height: 100%; background: none; }

.home-hero-bar input::-webkit-slider-thumb {
  width: 9px;
  height: 14px;
  appearance: none;
  border: 0;
  border-radius: 1px;
  background: var(--ink);
}

.home-hero-bar input::-moz-range-thumb {
  width: 9px;
  height: 14px;
  border: 0;
  border-radius: 1px;
  background: var(--ink);
}

.home-hero-bar input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Glyph transport, not labelled buttons — three uppercase words carried more
   weight than the timeline they controlled. Inline SVG, because ▶ and ⏸ render
   at wildly different weights across platforms. */
.home-hero-chrome-controls button {
  display: grid;
  width: 26px;
  height: 26px;
  appearance: none;
  border: 0;
  border-radius: 50%;
  padding: 0;
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  place-items: center;
  transition: color 160ms ease, background 160ms ease;
}

.home-hero-chrome-controls button:hover { color: var(--accent); background: rgba(22, 50, 79, .06); }
.home-hero-chrome-controls button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.home-hero-chrome-controls button svg { display: block; width: 12px; height: 12px; fill: currentColor; }

/* One button, two glyphs: whichever the next action is. */
.home-hero-chrome-controls [data-hero-toggle] .ico-pause { display: none; }
.home-hero-chrome-controls [data-hero-toggle].is-playing .ico-play { display: none; }
.home-hero-chrome-controls [data-hero-toggle].is-playing .ico-pause { display: block; }

.home-hero-clock {
  text-align: right;
  color: var(--soft);
  font: 500 9.5px/1 "IBM Plex Mono", monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: .08em;
  white-space: nowrap;
}

.home-hero-clock b { color: var(--ink); font-weight: 600; }

/* ---- responsive ---- */

@media (max-width: 980px) {
  .home-hero::before {
    inset: 0 0 64px 0;
    background: linear-gradient(180deg,
      rgba(250, 248, 243, .95) 0%,
      rgba(250, 248, 243, .7) 34%,
      rgba(250, 248, 243, 0) 62%);
  }
  .home-hero-title { width: auto; right: var(--gutter); }
  .home-hero-panel { width: auto; right: var(--gutter); }
  .home-hero-panel-body { height: 200px; }
}

@media (max-width: 640px) {
  .home-hero { --gutter: 18px; height: min(92vh, 720px); min-height: 600px; }
  .home-hero-title { top: 40px; }
  .home-hero-title h1 { font-size: clamp(27px, 7.4vw, 38px); line-height: 1.06; }
  .home-hero-lede { display: none; }
  .home-hero-panel { bottom: 78px; }
  .home-hero-panel-body { height: 176px; }
  .home-hero-ask p { font-size: 14px; }
  .home-hero-src li { font-size: 9.5px; }
  /* Nine still lines do not fit a short phone panel; show the first six. */
  .home-hero-src li:nth-child(n + 7) { display: none; }
  .home-hero-chrome { grid-template-columns: auto minmax(0, 1fr) 74px; gap: 10px; }
  .home-hero-beat { display: none; }
  .home-hero-chrome-controls button {
    width: 44px;
    height: 44px;
  }
  .home-hero-bar { height: 44px; }
  .home-hero-bar::before,
  .home-hero-bar::after { top: 21px; }
  .home-hero-bar-tick { top: 17px; }
  .home-hero-bar input::-webkit-slider-thumb { height: 24px; margin-top: 10px; }
  .home-hero-bar input::-moz-range-thumb { height: 24px; }
  .home-hero-enable {
    min-height: 44px;
    padding-inline: 16px;
  }
}

.filter-shell {
  border-block: 1px solid var(--rule);
  background: rgba(255, 255, 255, 0.42);
}

.filters {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-block: 15px;
  overflow-x: auto;
  scrollbar-width: none;
}

.filters::-webkit-scrollbar {
  display: none;
}

.filter-label {
  flex: none;
  margin-right: 8px;
  color: var(--soft);
}

/* A second group of chips needs air in front of its label, or the two axes
   read as one long undifferentiated row. */
.filters .filter + .filter-label {
  margin-left: 14px;
}

.filter {
  flex: none;
  appearance: none;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  color: var(--ink-2);
  cursor: pointer;
  padding: 8px 11px;
  font: 500 10.5px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.04em;
  transition: border-color 160ms ease, color 160ms ease, background 160ms ease;
}

.filter:hover {
  border-color: var(--soft);
  color: var(--ink);
}

.filter[aria-pressed="true"] {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper);
}

.gallery {
  padding: 44px 0 96px;
}

.result-count {
  min-height: 18px;
  margin: 0 0 20px;
  color: var(--soft);
  font: 500 10.5px / 1.4 "IBM Plex Mono", monospace;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 36px 22px;
}

.example-card {
  min-width: 0;
}

.example-card[hidden] {
  display: none;
}

.card-link {
  display: block;
  text-decoration: none;
}

.card-cover {
  position: relative;
  display: grid;
  aspect-ratio: 4 / 3;
  place-items: center;
  overflow: hidden;
  padding: 14px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  box-shadow: var(--shadow);
  transition: border-color 180ms ease;
}

.card-link:hover .card-cover {
  border-color: var(--accent);
}

/* The cover drawings carry their own generous paper margin — the audit in
   scripts/audit-cover-luma.mjs measures it at 54% of the mat on every model —
   and at card size that margin was over half of each tile spent on nothing.
   Scaled up, the buildings fill the grid, and the card's two pictures finally
   agree: a rendering resting at nearly full frame used to hand over, on
   hover, to a drawing half its size, which read as the card shrinking rather
   than as the same building drawn.

   1.3 is the largest scale at which no BUILDING in the gallery is cut. Some
   parcel and road does leave the frame, which is what the site plan sheet is
   for; the subject never does. A model whose cover crops its building is a
   model this number is wrong for, not a reason to crop it. */
.card-cover svg {
  width: 100%;
  height: 100%;
  object-fit: contain;
  transform: scale(1.3);
}

/* The photorealistic counterpart of a card's own picture — a generated
   rendering on an example, a real-world reference on an element. It occupies
   the whole frame and hover or keyboard focus swaps it with what is beneath;
   cardPhoto() in scripts/build-site-html.mjs carries the reasoning, including
   why the examples grid rests on the rendering and the elements grid does not.

   On a cover the photograph takes the same paper margin the drawings take.
   Bled to the edges it was the only card in the grid without a mat, and one
   photograph among nine drawings read as a different product rather than as
   the same card with a rendering. Sharing the mat, it reads as a print
   mounted on the same sheet — which is what it is.

   A card whose counterpart does not exist carries no element at all: no
   placeholder, no reserved gap, no announcement of an absence. */
.card-photo {
  position: absolute;
  top: 5%;
  left: 6%;
  width: 88%;
  height: 90%;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  object-fit: cover;
  transition: opacity 240ms ease;
}

.card-link:hover .card-photo,
.card-link:focus-visible .card-photo {
  opacity: 0;
}

/* An element tile is square and bled, so its counterpart is too — there is no
   sheet on that page to share a margin with. */
.element-stage .card-photo {
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* The swap is the point; the fade is manners. */
@media (prefers-reduced-motion: reduce) {
  .card-photo {
    transition: none;
  }
}

.validation-badge {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: 1px solid #cfdcc2;
  border-radius: var(--radius);
  background: #eef3e8;
  color: var(--pass);
  font: 600 10px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.validation-badge::before {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  content: "";
}

.validation-badge.fail {
  border-color: #e2bcb3;
  background: #f9ebe8;
  color: var(--fail);
}

.validation-badge.warning {
  border-color: #e4cca9;
  background: #fbf3e7;
  color: var(--warning);
}

.card-body {
  padding: 16px 2px 0;
}

.card-meta {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  color: var(--soft);
}

.card-body h2 {
  margin: 9px 0 7px;
  font: 400 25px / 1.08 "Instrument Serif", serif;
  letter-spacing: -0.01em;
}

.card-description {
  margin: 0 0 8px;
  overflow: hidden;
  color: var(--soft);
  font: 400 13px / 1.45 "IBM Plex Sans", sans-serif;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-count {
  margin: 0;
  color: var(--ink-2);
  font: 500 11.5px / 1.5 "IBM Plex Mono", monospace;
}

.hero3d {
  position: relative;
  height: min(86vh, 860px);
  min-height: 520px;
  overflow: hidden;
  border-bottom: 1px solid var(--rule);
  /* Shown for the frame the iframe takes to paint. A blue-grey flashed a colour
     the model register no longer contains; the rig's own field is paper. */
  background: #f0eee7;
}

.hero3d iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* The live model. example-3d.js compiles the NDJSON in the browser and draws it
   through the same lib/scene-solids.mjs the inspector uses, so this canvas and
   the inspector cannot disagree about what a view reveals. */
.hero3d .hero-live {
  position: absolute;
  inset: 0;
  touch-action: none;
}

.hero3d .hero-live canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* The poster leads. It is the canonical axonometric SVG — right immediately,
   right without WebGL, right while the compiler runs — and it steps behind the
   canvas only once a real frame exists. */
.hero3d .hero-plate {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* The veil washes out the left third, which is enough behind a live 3D model
     filling the frame but not behind a contained drawing — the title landed
     across the building. Reserving a column instead shrank the drawing to a
     thumbnail, so the title takes a band off the top and the drawing keeps the
     full width beneath it. Bottom padding clears the view picker. */
  object-position: center bottom;
  padding: clamp(180px, 30vh, 300px) clamp(16px, 4vw, 56px) 104px;
  box-sizing: border-box;
  transition: opacity 420ms ease;
}

.hero3d .hero-plate[data-state="behind"] {
  opacity: 0;
  pointer-events: none;
}

/* Type legibility, not a curtain. At 94% opacity reaching 60% of the width
   this erased the left third of every model — on a front axonometric that is
   the entrance facade, so the one elevation a visitor most wants to see was
   the one the page painted over. The wash now dies at 44%, and a light top
   band carries the eyebrow, so it covers the title block rather than the
   building. Any further legibility should come from the title's own weight. */
.hero3d .veil {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(
      90deg,
      rgba(250, 248, 243, 0.90) 0%,
      rgba(250, 248, 243, 0.56) 17%,
      rgba(250, 248, 243, 0.13) 32%,
      rgba(250, 248, 243, 0) 44%
    ),
    linear-gradient(
      180deg,
      rgba(250, 248, 243, 0.32) 0%,
      rgba(250, 248, 243, 0) 32%
    );
  pointer-events: none;
}

.hero-title {
  position: absolute;
  inset: 7vh 0 auto;
  pointer-events: none;
  animation: rise 700ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
}

.crumb {
  margin-bottom: 16px;
  color: var(--soft);
}

.crumb a {
  color: var(--accent);
  pointer-events: auto;
  text-decoration: none;
}

.hero-title h1 {
  max-width: 17ch;
  margin: 0;
  font: 400 clamp(40px, 6vw, 76px) / 0.98 "Instrument Serif", serif;
  letter-spacing: -0.018em;
  text-wrap: balance;
}

.hero-title h1 em {
  color: var(--accent);
  font-style: italic;
}

.drag-note {
  position: absolute;
  right: 28px;
  bottom: 158px;
  color: var(--soft);
  pointer-events: none;
}

.drag-note::before {
  display: inline-block;
  width: 22px;
  height: 1px;
  margin-right: 9px;
  background: var(--soft);
  content: "";
  vertical-align: middle;
}

.hero-view-picker {
  position: absolute;
  z-index: 7;
  left: 50%;
  bottom: 14px;
  width: min(calc(100% - 48px), var(--wrap));
  pointer-events: none;
  transform: translateX(-50%);
}

.hero-detail-control {
  display: flex;
  align-items: center;
  width: max-content;
  margin: 0 3px 4px auto;
  overflow: hidden;
  border: 1px solid rgba(22, 50, 79, 0.18);
  border-radius: var(--radius);
  background: rgba(250, 248, 243, 0.96);
  box-shadow: 0 5px 18px rgba(22, 50, 79, 0.08);
  pointer-events: auto;
}

.hero-detail-control > span {
  padding: 0 10px;
  color: var(--soft);
  font: 600 8px / 30px "IBM Plex Mono", monospace;
  letter-spacing: 0.12em;
}

.hero-detail-control button {
  height: 30px;
  appearance: none;
  border: 0;
  border-left: 1px solid var(--rule);
  background: transparent;
  color: var(--soft);
  cursor: pointer;
  font: 600 8px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.055em;
  padding: 0 9px;
}

.hero-detail-control button:hover {
  color: var(--ink);
}

.hero-detail-control button[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
}

.hero-detail-control button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -3px;
}

.hero-view-strip {
  display: grid;
  grid-auto-columns: 112px;
  grid-auto-flow: column;
  gap: 7px;
  overflow-x: auto;
  padding: 4px 3px 8px;
  pointer-events: auto;
  scroll-snap-type: x proximity;
  scrollbar-color: var(--rule) transparent;
}

.model-view-card {
  min-width: 0;
  appearance: none;
  overflow: hidden;
  border: 1px solid rgba(22, 50, 79, 0.18);
  border-radius: var(--radius);
  background: rgba(250, 248, 243, 0.96);
  box-shadow: 0 5px 18px rgba(22, 50, 79, 0.08);
  color: var(--ink);
  cursor: pointer;
  padding: 0;
  scroll-snap-align: start;
  text-align: left;
  transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
}

.model-view-card:hover {
  border-color: var(--soft);
  transform: translateY(-2px);
}

.model-view-card[aria-pressed="true"] {
  border-color: rgba(22, 50, 79, 0.24);
  box-shadow: inset 0 -3px 0 var(--accent), 0 6px 20px rgba(22, 50, 79, 0.12);
}

.model-view-card:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.model-view-image {
  display: block;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
}

.model-view-image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.model-view-label {
  display: block;
  overflow: hidden;
  padding: 7px 8px 8px;
  font: 600 9px / 1.2 "IBM Plex Mono", monospace;
  letter-spacing: 0.06em;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
}

.intro {
  padding: 52px 0 30px;
}

.lede {
  max-width: 68ch;
  margin: 0 0 28px;
  color: var(--ink-2);
  font-size: 16.5px;
}

.boundary-note {
  max-width: 68ch;
  margin: -12px 0 28px;
  border-left: 2px solid var(--accent);
  padding-left: 14px;
  color: var(--soft);
  font: 500 12px / 1.5 "IBM Plex Mono", monospace;
  text-transform: lowercase;
}

.stats {
  display: flex;
  flex-wrap: wrap;
  border-block: 1px solid var(--rule);
}

.stat {
  margin-right: 26px;
  border-right: 1px solid var(--rule);
  padding: 14px 26px 14px 0;
}

.stat:last-child {
  border-right: 0;
}

.stat b {
  display: block;
  font: 600 19px / 1.2 "IBM Plex Mono", monospace;
}

.stat span {
  color: var(--soft);
  font: 500 10.5px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}

.stat.pass b {
  color: var(--pass);
}

.model-renderings {
  padding: 22px 0 36px;
}

.renderings-head {
  display: grid;
  grid-template-columns: minmax(220px, 0.75fr) minmax(0, 1.25fr);
  gap: var(--space-3);
  align-items: end;
  margin-bottom: var(--space-2);
}

.renderings-kicker {
  margin: 0 0 7px;
  color: var(--accent);
  font: 600 10px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.renderings-statement {
  max-width: 68ch;
  margin: 0;
  color: var(--ink-2);
  font-size: 14px;
}

/* One column per rendering, always one row: the count comes from how many of
   the model's views were photographed, and that is no longer a fixed three. */
.renderings-grid {
  display: grid;
  grid-template-columns: repeat(var(--rendering-count, 3), minmax(0, 1fr));
  border: 1px solid var(--rule);
  background: var(--sheet);
  box-shadow: var(--shadow);
}

.rendering {
  min-width: 0;
  margin: 0;
  border-right: 1px solid var(--rule);
}

.rendering:last-child {
  border-right: 0;
}

.rendering-media {
  position: relative;
  overflow: hidden;
  aspect-ratio: 3 / 2;
  background: var(--paper);
}

.rendering img,
.rendering video {
  display: block;
  width: 100%;
  height: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

.rendering video {
  position: absolute;
  inset: 0;
}

.rendering figcaption {
  border-top: 1px solid var(--rule);
  padding: 11px 13px 12px;
  color: var(--ink);
  font: 600 10px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.11em;
  text-transform: uppercase;
}

/* The visual proof hands directly to the files that carry it elsewhere. A
   ruled head keeps the register distinct from the rendering frame without
   inserting another full-width colour band between the two. */
.model-downloads {
  padding: 0 0 56px;
}

.floorplans {
  padding: 18px 0 54px;
}

.model-downloads-head,
.floorplans-head {
  display: grid;
  grid-template-columns: minmax(220px, 0.75fr) minmax(0, 1.25fr);
  gap: var(--space-3);
  align-items: end;
  margin-bottom: var(--space-2);
}

.model-downloads-head {
  border-top: 1px solid var(--rule);
  padding-top: 22px;
}

.model-downloads-kicker {
  margin: 0 0 7px;
  color: var(--accent);
  font: 600 10px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.model-downloads-statement {
  max-width: 68ch;
  margin: 0 0 10px;
  color: var(--ink-2);
  font-size: 14px;
}

.floorplans-statement {
  max-width: 68ch;
  margin: 0;
  color: var(--ink-2);
  font-size: 14px;
}

.floorplan-spread {
  display: grid;
  grid-template-columns: repeat(var(--floorplan-count, 2), minmax(0, 1fr));
  border: 1px solid var(--rule);
  background: var(--sheet);
  box-shadow: var(--shadow);
}

.floorplan-leaf {
  min-width: 0;
  border-right: 1px solid var(--rule);
}

.floorplan-leaf:last-child {
  border-right: 0;
}

.floorplan-sheet {
  display: flex;
  width: 100%;
  min-height: 360px;
  align-items: center;
  justify-content: center;
  appearance: none;
  border: 0;
  border-radius: 0;
  background: var(--sheet);
  color: inherit;
  cursor: zoom-in;
  padding: clamp(16px, 2.6vw, 34px);
  transition: background 160ms ease;
}

.floorplan-sheet:hover {
  background: var(--paper);
}

.floorplan-sheet:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -3px;
}

.floorplan-sheet svg {
  display: block;
  width: 100%;
  height: auto;
  max-height: 520px;
}

.floorplan-caption {
  display: grid;
  grid-template-columns: minmax(120px, 0.42fr) minmax(0, 1fr);
  gap: 18px;
  min-height: 76px;
  border-top: 1px solid var(--rule);
  padding: 14px 16px 16px;
}

.floorplan-caption strong {
  color: var(--ink);
  font: 600 10px / 1.25 "IBM Plex Mono", monospace;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.floorplan-caption span {
  color: var(--soft);
  font-size: 12px;
  line-height: 1.45;
}

.plates {
  padding: 12px 0 36px;
}

.plate-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(230px, 300px);
  gap: 40px;
  align-items: start;
  border-top: 1px solid var(--rule);
  padding: 44px 0;
}

.plate-row:first-child {
  border-top: 0;
}

.plate-row.flip {
  grid-template-columns: minmax(230px, 300px) minmax(0, 1fr);
}

.plate-row.flip .paperbox {
  order: 2;
}

/* A plate is a drawing you can open, the same as a card in the index below it.
   It was the only drawing on the page that could not be. */
.paperbox {
  display: flex;
  min-width: 0;
  min-height: 220px;
  align-items: center;
  justify-content: center;
  appearance: none;
  border: 1px solid var(--rule);
  border-radius: 0;
  background: var(--sheet);
  box-shadow: var(--shadow);
  color: inherit;
  cursor: zoom-in;
  font: inherit;
  padding: 20px;
  text-align: left;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}

.paperbox:hover {
  border-color: var(--soft);
  box-shadow: 0 10px 26px rgba(22, 50, 79, 0.14);
}

.paperbox:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.paperbox svg {
  display: block;
  max-width: 100%;
  height: auto;
}

.plate-copy {
  padding-top: 6px;
}

.plate-index {
  color: var(--accent);
}

.plate-copy h2 {
  margin: 11px 0 10px;
  font: 400 27px / 1.12 "Instrument Serif", serif;
  letter-spacing: -0.01em;
}

.plate-copy p {
  margin: 0;
  color: var(--ink-2);
  font-size: 13.8px;
}

/* The sheet index. Four plates carry the argument at full width; the remaining
   twelve to twenty-four drawings are thumbnails here, the way the first page of
   a construction set indexes what the set contains. The card borrows the hero
   picker's card language on purpose — one thumbnail idea on the page, not two. */
.index-head {
  border-top: 1px solid var(--rule);
  padding-top: var(--space-3);
  margin-top: var(--space-2);
}

/* The census: what the discipline headings used to say, in one line. */
.index-census {
  border-top: 1px solid var(--rule);
  color: var(--soft);
  font: 400 var(--caption) / 1.9 "IBM Plex Mono", monospace;
  letter-spacing: 0.06em;
  padding-top: 10px;
  margin: var(--space-2) 0 var(--space-2);
}

.index-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(178px, 1fr));
  gap: 12px;
}

.index-card {
  display: flex;
  min-width: 0;
  flex-direction: column;
  appearance: none;
  overflow: hidden;
  cursor: zoom-in;
  padding: 0;
  text-align: left;
  border: 1px solid rgba(22, 50, 79, 0.18);
  border-radius: var(--radius);
  background: var(--sheet);
  box-shadow: 0 5px 18px rgba(22, 50, 79, 0.08);
  color: var(--ink);
  transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
}

.index-card:hover {
  border-color: var(--soft);
  box-shadow: 0 10px 26px rgba(22, 50, 79, 0.14);
  transform: translateY(-2px);
}

.index-card:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

/* aspect-ratio sets the frame's starting height; `flex: 1` then lets it take
   the rest of the card, so every caption in a row sits on the same line however
   tall the drawing inside wanted to be. */
.index-frame {
  display: flex;
  min-height: 0;
  flex: 1;
  aspect-ratio: 4 / 3;
  align-items: center;
  justify-content: center;
  border-bottom: 1px solid var(--rule);
  background: var(--sheet);
  padding: 8px;
}

/* viewBox letterboxes the drawing inside the card; the same node reflows to the
   dialog's stage without a second copy. */
.index-frame svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* The sheet number rides on the card, which is what makes the discipline
   headings redundant: the grid stays sorted in drawing-set order, so the
   numbers themselves mark where one discipline ends and the next begins. */
/* Pinned to two lines so every card in a row is the same height whatever its
   drawing is called; the full name is in the tooltip and in the dialog head. */
.index-caption {
  display: -webkit-box;
  overflow: hidden;
  min-height: calc(var(--caption) * 1.45 * 2 + 17px);
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  background: var(--paper);
  color: var(--ink-2);
  font: 400 var(--caption) / 1.45 "IBM Plex Mono", monospace;
  padding: 8px 9px 9px;
}

.index-caption b {
  color: var(--accent);
  font-weight: 500;
  letter-spacing: 0.06em;
}

.index-caption b::after {
  color: var(--rule);
  content: " · ";
}

/* An issued sheet composes several of the views above it and reuses their
   number, so it says so. */
.is-sheet .index-caption b::before {
  color: var(--soft);
  content: "SHEET ";
}

.viewer {
  overflow: hidden;
  width: min(1440px, 94vw);
  max-width: none;
  max-height: 92vh;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: 0 30px 80px -20px rgba(18, 22, 28, 0.5);
  color: var(--ink);
  padding: 0;
}

.viewer::backdrop {
  background: rgba(18, 22, 28, 0.72);
}

.viewer-head {
  display: flex;
  gap: var(--space-2);
  align-items: baseline;
  border-bottom: 1px solid var(--rule);
  background: var(--sheet);
  padding: 13px var(--space-2);
}

.viewer-number {
  color: var(--accent);
  font: 500 var(--caption) / 1.2 "IBM Plex Mono", monospace;
  letter-spacing: 0.08em;
  white-space: nowrap;
}

/* The caption claims the slack, so the formats and Close sit hard right whether
   or not the drawing has any formats to offer. */
.viewer-caption {
  overflow: hidden;
  margin-right: auto;
  font: 400 17px / 1.3 "Instrument Serif", serif;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A drawing's formats print in the head that already names it — the title-block
   row of the sheet you opened. Cards and plates stay linework: a sheet index
   does not carry a toolbar under every thumbnail, and a new format here is one
   more link rather than a taller card. Close keeps the only box in the row, so
   saving and dismissing never compete for the same read. */
.viewer-formats {
  display: flex;
  gap: 14px;
  align-items: baseline;
  white-space: nowrap;
}

/* `display: flex` outranks the UA's `[hidden] { display: none }`, so a drawing
   with no formats would print an empty group without this. */
.viewer-formats[hidden] {
  display: none;
}

.viewer-formats-label {
  color: var(--soft);
  font: 500 var(--caption) / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.08em;
  margin-right: -4px;
  text-transform: uppercase;
}

.viewer-formats a {
  border-bottom: 1px solid var(--rule);
  color: var(--ink-2);
  font: 500 var(--caption) / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.08em;
  padding-bottom: 3px;
  text-decoration: none;
  text-transform: uppercase;
  transition: border-color 160ms ease, color 160ms ease;
}

.viewer-formats a:hover {
  border-bottom-color: var(--accent);
  color: var(--accent);
}

.viewer-formats a:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.viewer-close {
  appearance: none;
  margin-left: var(--space-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink-2);
  cursor: pointer;
  font: 500 var(--caption) / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.08em;
  padding: 7px 11px;
  text-transform: uppercase;
  transition: border-color 160ms ease, color 160ms ease;
}

.viewer-close:hover {
  border-color: var(--soft);
  color: var(--ink);
}

.viewer-stage {
  display: flex;
  overflow: auto;
  max-height: calc(92vh - 54px);
  align-items: center;
  justify-content: center;
  background: var(--sheet);
  padding: var(--space-2);
}

.viewer-stage svg {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: calc(92vh - 54px - var(--space-3));
}

@media (max-width: 640px) {
  .index-grid {
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  }

  .viewer {
    width: 96vw;
  }

  /* Narrow, the head wraps rather than squeezing the drawing's name to an
     ellipsis; the formats take their own row and scroll if a set ever carries
     more than a phone can print. */
  .viewer-head {
    flex-wrap: wrap;
  }

  .viewer-formats {
    order: 3;
    overflow-x: auto;
    width: 100%;
  }
}

.proof {
  margin: 54px 0 0;
  border-block: 1px solid var(--void-line);
  background: var(--void);
  color: var(--lit);
  padding: 64px 0 72px;
}

.section-heading {
  margin: 0 0 10px;
  font: 400 var(--headline) / 1.02 "Instrument Serif", serif;
  letter-spacing: -0.015em;
}

.section-intro {
  max-width: 66ch;
  margin: 0 0 30px;
  color: var(--lit-dim);
}

.rig {
  display: grid;
  grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
  gap: 30px;
  align-items: start;
}

.knobs {
  border: 1px solid var(--void-line);
  border-radius: var(--radius);
  background: var(--void-2);
  padding: 19px 18px 15px;
}

.knob {
  margin-bottom: 19px;
}

.knob:last-of-type {
  margin-bottom: 10px;
}

.knob label {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  align-items: baseline;
  margin-bottom: 8px;
  color: var(--lit-dim);
  font: 500 10.5px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.11em;
  text-transform: uppercase;
}

.knob output {
  color: var(--accent-lit);
  font: 600 13px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0;
}

.knob input[type="range"] {
  width: 100%;
  accent-color: var(--accent-lit);
}

.model-line {
  margin-top: 9px;
  overflow-x: auto;
  border: 1px solid var(--void-line);
  border-radius: var(--radius);
  background: var(--void);
  color: var(--lit-dim);
  padding: 7px 9px;
  font: 500 10.5px / 1.5 "IBM Plex Mono", monospace;
  white-space: nowrap;
}

.model-line b {
  color: var(--accent-lit);
}

.live-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

.live {
  overflow: hidden;
  border: 1px solid var(--void-line);
  border-radius: var(--radius);
  background: var(--sheet);
}

.live h3 {
  margin: 0;
  border-bottom: 1px solid var(--rule);
  background: #fdfcf9;
  color: var(--soft);
  padding: 10px 14px;
  font: 500 10.5px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}

.live-box {
  display: flex;
  min-height: 210px;
  align-items: center;
  justify-content: center;
  padding: 12px;
}

.live-box svg {
  max-width: 100%;
  height: auto;
}

.verdict {
  min-height: 38px;
  margin-top: 15px;
  color: var(--pass);
  font: 500 11.5px / 1.5 "IBM Plex Mono", monospace;
}

.verdict.bad {
  color: #e77b64;
}

.verdict.warn {
  color: var(--accent-lit);
}

.site-note {
  max-width: 70ch;
  margin: 18px 0 0;
  border-left: 2px solid var(--accent-lit);
  padding-left: 18px;
  color: var(--lit-dim);
}

.meta-section {
  padding: 72px 0 80px;
}

.meta-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 22px;
  margin-bottom: 18px;
}

.meta-actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* One model, four hand-off contracts. The joined compartments borrow the
   grammar of an architectural title block: role above, format large, delivery
   facts on the baseline. They are deliberately not four floating product
   cards — export is part of the model record, not an upsell. */
.model-deliverables {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin: 0;
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  box-shadow: var(--shadow);
}

.model-deliverable {
  position: relative;
  display: flex;
  min-width: 0;
  min-height: 176px;
  flex-direction: column;
  padding: 17px 18px 15px;
  border-left: 1px solid var(--rule);
}

.model-deliverable:first-child {
  border-left: 0;
}

.model-deliverable.is-featured::before {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  height: 3px;
  background: var(--accent);
  content: "";
}

.model-deliverable-head,
.model-deliverable-foot {
  display: flex;
  align-items: center;
  gap: 12px;
}

.model-deliverable-head {
  justify-content: space-between;
  min-height: 17px;
}

.model-deliverable-role,
.model-deliverable-badge,
.model-deliverable-foot {
  font: 500 10px / 1.2 "IBM Plex Mono", monospace;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.model-deliverable-role,
.model-deliverable-foot > span {
  color: var(--soft);
}

.model-deliverable-badge {
  color: var(--accent);
  letter-spacing: 0.05em;
}

.model-deliverable h3 {
  margin: 12px 0 5px;
  color: var(--ink);
  font: 500 22px / 1 "IBM Plex Mono", monospace;
  letter-spacing: -0.025em;
}

.model-deliverable p {
  max-width: 34ch;
  margin: 0 0 14px;
  color: var(--ink-2);
  font: 400 13px / 1.48 "IBM Plex Sans", sans-serif;
}

/* Compatibility reads like a drawing legend, not a partner-logo wall. Official
   marks carry recognition while adjacent product names make the claim explicit. */
.model-compatible {
  display: grid;
  gap: 7px;
  margin: 0 0 15px;
}

.model-compatible-label {
  color: var(--soft);
  font: 500 8.5px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.model-compatible ul {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 7px 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.model-compatible li {
  display: inline-flex;
  min-width: 0;
  align-items: center;
  gap: 5px;
  color: var(--ink-2);
  font: 500 9px / 1.2 "IBM Plex Sans", sans-serif;
  white-space: nowrap;
}

.model-app-mark {
  display: inline-grid;
  width: 20px;
  height: 20px;
  flex: 0 0 20px;
  place-items: center;
  border: 1px solid color-mix(in srgb, var(--ink) 20%, var(--rule));
  border-radius: 4px;
  background:
    linear-gradient(135deg, transparent 0 54%, color-mix(in srgb, var(--accent) 10%, transparent) 54%),
    var(--paper);
  color: var(--ink);
  font: 600 7.5px / 1 "IBM Plex Mono", monospace;
  letter-spacing: -0.04em;
}

.model-app-mark img {
  display: block;
  width: 14px;
  height: 14px;
  object-fit: contain;
}

.model-deliverable.is-featured .model-app-mark {
  border-color: color-mix(in srgb, var(--accent) 42%, var(--rule));
}

.model-deliverable-foot {
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid var(--rule);
}

.model-deliverable-foot > span {
  margin-right: auto;
}

.model-deliverable-foot a,
.model-deliverable-foot button {
  appearance: none;
  border: 0;
  border-bottom: 1px solid transparent;
  background: none;
  color: var(--ink);
  cursor: pointer;
  font: inherit;
  letter-spacing: inherit;
  padding: 3px 0;
  text-decoration: none;
  text-transform: inherit;
  transition: border-color 160ms ease, color 160ms ease;
}

.model-deliverable-foot a:hover,
.model-deliverable-foot button:hover {
  border-bottom-color: var(--accent);
  color: var(--accent);
}

.model-deliverable-foot a:focus-visible,
.model-deliverable-foot button:focus-visible {
  outline-offset: 2px;
}

.copy-button {
  appearance: none;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  color: var(--ink);
  cursor: pointer;
  padding: 9px 12px;
  font: 500 10.5px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.copy-button:hover {
  border-color: var(--accent);
}

.meta-cols {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(280px, 1fr);
  gap: 34px;
  align-items: start;
}

.source {
  max-height: 430px;
  margin: 0;
  overflow: auto;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  box-shadow: var(--shadow);
  color: var(--ink-2);
  padding: 16px 18px;
  font: 400 11.5px / 1.75 "IBM Plex Mono", monospace;
  tab-size: 2;
}

.source .keyword {
  color: var(--accent);
}

.source .comment {
  color: var(--soft);
  font-style: italic;
}

.validation {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  box-shadow: var(--shadow);
  padding: 20px;
}

.validation-badge {
  margin-bottom: 16px;
  padding: 8px 12px;
}

.rules,
.diagnostics {
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--ink-2);
  font: 400 12px / 1.5 "IBM Plex Mono", monospace;
}

.rules li {
  display: flex;
  gap: 10px;
  border-bottom: 1px dotted var(--rule);
  padding: 7px 0;
}

.rules li:last-child {
  border-bottom: 0;
}

.rules span {
  flex: none;
  color: var(--pass);
}

.diagnostics {
  margin-bottom: 14px;
}

.diagnostics li {
  border-left: 2px solid var(--warning);
  margin-bottom: 10px;
  padding-left: 11px;
}

.diagnostics li.error {
  border-color: var(--fail);
}

.diagnostics b {
  display: block;
  color: var(--ink);
}

.diagnostics small {
  display: block;
  margin-top: 3px;
  color: var(--soft);
  font: inherit;
}

.lesson {
  margin-top: 26px;
  border-left: 2px solid var(--accent);
  background: #f6f2e9;
  padding: 20px 22px;
}

.lesson h3 {
  margin: 0 0 8px;
  color: var(--accent);
  font: 500 10.5px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.lesson p {
  margin: 0;
  color: var(--ink-2);
  font-size: 14.5px;
}

.prev-next {
  border-top: 1px solid var(--rule);
  padding: 30px 0 52px;
}

.prev-next .wrap {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 22px;
}

.next-link {
  display: block;
  border-bottom: 1px solid var(--rule);
  padding: 14px 0;
  text-decoration: none;
}

.next-link:last-child {
  text-align: right;
}

.next-link .label {
  display: block;
  margin-bottom: 5px;
  color: var(--soft);
}

.next-link strong {
  font: 400 21px / 1.2 "Instrument Serif", serif;
}

.site-footer {
  border-top: 1px solid var(--void-line);
  background: var(--void);
  color: var(--lit-dim);
  padding: 34px 0 42px;
}

.site-footer .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 22px;
}

.footer-brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--lit);
  font: 400 23px / 1 "Instrument Serif", serif;
  text-decoration: none;
}

.footer-brand img {
  display: block;
  width: 36px;
  height: 36px;
}

.footer-brand em {
  color: var(--accent-lit);
  font-style: italic;
}

.footer-copy {
  display: grid;
  gap: 6px;
  text-align: right;
}

.site-footer a {
  color: var(--lit);
  text-decoration-color: var(--void-line);
  text-underline-offset: 3px;
}

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

@media (max-width: 920px) {
  .card-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .rig,
  .meta-cols {
    grid-template-columns: 1fr;
  }

  .model-deliverables {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .model-deliverable:nth-child(odd) {
    border-left: 0;
  }

  .model-deliverable:nth-child(n + 3) {
    border-top: 1px solid var(--rule);
  }

}

@media (max-width: 700px) {
  .wrap {
    padding-inline: 18px;
  }

  /* The masthead's own small-screen rules are in web/masthead.css, beside the
     rest of it, so the two halves cannot be moved apart by accident. */

  .card-grid {
    grid-template-columns: 1fr;
    row-gap: 32px;
  }

  .model-renderings {
    padding-bottom: var(--space-3);
  }

  .renderings-head,
  .renderings-grid,
  .model-downloads-head,
  .floorplans-head,
  .floorplan-spread {
    grid-template-columns: 1fr;
  }

  .renderings-head,
  .model-downloads-head,
  .floorplans-head {
    gap: var(--space-1);
  }

  .model-downloads {
    padding-bottom: var(--space-3);
  }

  .rendering {
    border-right: 0;
    border-bottom: 1px solid var(--rule);
  }

  .rendering:last-child {
    border-bottom: 0;
  }

  .floorplan-leaf {
    border-right: 0;
    border-bottom: 1px solid var(--rule);
  }

  .floorplan-leaf:last-child {
    border-bottom: 0;
  }

  .floorplan-sheet {
    min-height: 280px;
    padding: 14px;
  }

  .floorplan-caption {
    grid-template-columns: 1fr;
    gap: 7px;
  }

  .hero3d {
    height: 74vh;
    min-height: 500px;
  }

  .hero-title {
    inset: 5vh 0 auto;
  }

  /* The title wraps to more lines here, so it needs a deeper band. */
  .hero3d .hero-plate {
    padding: 34vh clamp(12px, 4vw, 32px) 96px;
  }

  .drag-note {
    right: 18px;
    bottom: 148px;
  }

  .hero-view-picker {
    bottom: 10px;
    width: calc(100% - 24px);
  }

  .hero-detail-control > span {
    display: none;
  }

  .hero-detail-control button {
    padding: 0 7px;
    font-size: 7px;
  }

  .hero-view-strip {
    grid-auto-columns: 100px;
  }

  .plate-row,
  .plate-row.flip {
    grid-template-columns: 1fr;
    gap: 22px;
  }

  .plate-row.flip .paperbox {
    order: 0;
  }

  .paperbox {
    padding: 10px;
  }

  .live-grid {
    grid-template-columns: 1fr;
  }

  .stat {
    width: 50%;
    margin: 0;
    border-bottom: 1px solid var(--rule);
    padding-right: 14px;
  }

  .stat:nth-child(2n) {
    border-right: 0;
    padding-left: 14px;
  }

  .stat:last-child {
    border-bottom: 0;
  }

  .meta-head {
    align-items: flex-start;
    flex-direction: column;
  }

  .meta-actions {
    justify-content: flex-start;
  }

  .model-deliverables {
    grid-template-columns: 1fr;
  }

  .model-deliverable {
    min-height: 0;
    border-top: 1px solid var(--rule);
    border-left: 0;
  }

  .model-deliverable:nth-child(n) {
    border-left: 0;
  }

  .model-deliverable:first-child {
    border-top: 0;
  }

  .model-compatible ul {
    flex-wrap: wrap;
  }

  .prev-next .wrap,
  .site-footer .wrap {
    grid-template-columns: 1fr;
    flex-direction: column;
  }

  .site-footer .wrap {
    align-items: flex-start;
  }

  .footer-copy {
    text-align: left;
  }

  .next-link:last-child {
    text-align: left;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .rendering video {
    display: none;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ── Elements gallery ───────────────────────────────────────────────────────
   The appearance half of the gallery. A look has no drawing, so the card
   carries a tile the browser builds through the real interpreter instead of a
   pre-rendered cover. The stage keeps the cover's proportions and border so the
   two galleries read as one site. */
.element-intro {
  padding: 48px 0 26px;
}

.element-intro h1 {
  margin: 10px 0 18px;
  max-width: 18ch;
  font: 400 clamp(34px, 5vw, 58px) / 1.02 "Instrument Serif", serif;
  letter-spacing: -0.015em;
}

.element-lede {
  margin: 0 0 14px;
  max-width: 62ch;
  color: var(--ink-2);
  font: 400 15px / 1.62 "IBM Plex Sans", sans-serif;
}

/* The count and the credibility claim share one line, so the grid starts a
   row higher than a stacked pair would allow. */
.element-count-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 18px;
  flex-wrap: wrap;
}

.element-claim {
  margin: 0 0 20px;
  color: var(--soft);
  font: 400 13px / 1.5 "IBM Plex Sans", sans-serif;
}

.element-cmd {
  margin: 0 0 18px;
  padding: 12px 14px;
  overflow-x: auto;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  font: 500 12.5px / 1.4 "IBM Plex Mono", monospace;
}

.element-pack-downloads {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px;
  margin-top: 18px;
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--rule);
}

.element-pack-download {
  display: grid;
  min-width: 0;
  align-content: center;
  gap: 5px;
  background: var(--sheet);
  padding: 12px 14px;
  color: var(--ink);
  font: 500 11px / 1.35 "IBM Plex Mono", monospace;
  text-decoration: none;
  transition: background-color 160ms ease, color 160ms ease;
}

.element-pack-download > span {
  min-width: 0;
  overflow-wrap: anywhere;
}

.element-pack-download small {
  color: var(--soft);
  font: inherit;
  font-size: 9px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.element-pack-download:hover,
.element-pack-download:focus-visible {
  background: var(--ink);
  color: var(--paper);
}

.element-pack-download:hover small,
.element-pack-download:focus-visible small {
  color: var(--lit-dim);
}

.element-pack-download-single {
  width: min(100%, 430px);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
}

.element-pack-note {
  margin: -6px 0 20px;
  max-width: 74ch;
  color: var(--soft);
  font: 400 13px / 1.55 "IBM Plex Sans", sans-serif;
}

.element-pack-note[hidden] {
  display: none;
}

/* The reference sits below the grid, so it gets a rule to separate it from the
   thing the page is actually for. */
.element-writing {
  border-top: 1px solid var(--rule);
  padding: 44px 0 96px;
}

.element-writing h2 {
  margin: 0 0 16px;
  font: 400 32px / 1.06 "Instrument Serif", serif;
}

.element-card {
  min-width: 0;
}

.element-card[hidden] {
  display: none;
}

.element-stage {
  position: relative;
  display: grid;
  aspect-ratio: 1;
  place-items: center;
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  box-shadow: var(--shadow);
  transition: border-color 180ms ease;
}

.element-card:hover .element-stage {
  border-color: var(--accent);
}

.element-stage canvas {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 220ms ease;
}

.element-stage.is-lit canvas {
  opacity: 1;
}

/* The selector is the contract — it decides whether this definition dresses the
   tree in front of you — so it states its ink rather than inheriting it. */
.element-selector {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  padding: 3px 7px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: middle;
}

.element-copy {
  margin-top: 12px;
  appearance: none;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: none;
  color: var(--soft);
  cursor: pointer;
  /* Roomier than a filter chip: copying the line is the whole point of the
     card, so it is the one control on the page worth a real tap target. */
  padding: 11px 14px;
  font: 500 11px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.04em;
  transition: border-color 160ms ease, color 160ms ease;
}

.element-copy:hover,
.element-copy:focus-visible {
  border-color: var(--ink);
  color: var(--ink);
}


/* ── One element ────────────────────────────────────────────────────────────
   The detail page. A visitor arrives having already seen the result on a
   tile, so the page opens on the two things a tile cannot give: an orbit, and
   proof that the same definition holds at any size. */
.element-detail-hero {
  padding: 48px 0 8px;
}

.element-detail-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 32px;
  flex-wrap: wrap;
  margin-bottom: 24px;
}

.element-detail-head h1 {
  margin: 8px 0 14px;
  font: 400 clamp(30px, 4.4vw, 50px) / 1.04 "Instrument Serif", serif;
  letter-spacing: -0.015em;
}

.element-detail-head .home-hero-eyebrow a {
  color: inherit;
}

.element-facts {
  display: flex;
  gap: 26px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}

.element-facts li {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.element-facts b {
  color: var(--ink);
  font: 400 26px / 1 "Instrument Serif", serif;
}

.element-facts span {
  color: var(--soft);
  font: 500 10.5px / 1.3 "IBM Plex Mono", monospace;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.element-stage-live {
  aspect-ratio: auto;
  display: block;
  min-height: 320px;
  position: relative;
}

.element-stage-live canvas {
  display: block;
  width: 100%;
  opacity: 0;
  transition: opacity 260ms ease;
}

.element-stage-live.is-lit canvas {
  opacity: 1;
}

.element-stage-hint {
  position: absolute;
  right: 14px;
  bottom: 12px;
  z-index: 1;
  margin: 0;
  color: var(--soft);
  font: 500 10px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Reference imagery is a quiet appendix, not a second hero. Provenance sits in
   the utility column; the photograph stays visibly separate from the live
   viewport and from everything the compiler derives. */
.element-reference {
  border-top: 1px solid var(--rule);
  padding: 56px 0 76px;
}

.element-reference-layout {
  display: grid;
  grid-template-columns: minmax(180px, 240px) minmax(0, 820px);
  align-items: start;
  gap: clamp(30px, 5vw, 64px);
}

.element-reference-label {
  margin: 3px 0 10px;
  color: var(--accent);
  font: 500 10px / 1.4 "IBM Plex Mono", monospace;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.element-reference h2 {
  margin: 0;
  font: 400 32px / 1.06 "Instrument Serif", serif;
}

.element-reference-frame {
  width: 100%;
  margin: 0;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
}

.element-reference-frame img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.element-reference-frame figcaption {
  border-top: 1px solid var(--rule);
  padding: 14px 16px 16px;
  color: var(--ink-2);
  font: 400 13px / 1.55 "IBM Plex Sans", sans-serif;
}

.element-paste {
  padding: 52px 0 72px;
}

.element-paste h2 {
  margin: 0 0 14px;
  font: 400 32px / 1.06 "Instrument Serif", serif;
}

/* Finishes are a parameter of one design, so they sit with the model they
   change rather than in a section of their own. */
.element-finish {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  appearance: none;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  color: var(--ink-2);
  cursor: pointer;
  padding: 7px 11px 7px 8px;
  font: 500 10.5px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.04em;
  transition: border-color 160ms ease, color 160ms ease;
}

.element-finish:hover {
  border-color: var(--soft);
  color: var(--ink);
}

.element-finish[aria-pressed="true"] {
  border-color: var(--ink);
  color: var(--ink);
}

.element-finish-chip {
  width: 16px;
  height: 16px;
  border: 1px solid rgba(22, 50, 79, 0.18);
  border-radius: 2px;
}

.element-finish-note {
  margin: 14px 0 0;
  max-width: 62ch;
  color: var(--soft);
  font: 400 13px / 1.55 "IBM Plex Sans", sans-serif;
}

.element-source {
  margin: 0 0 14px;
  padding: 16px 18px;
  overflow-x: auto;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  color: var(--ink-2);
  font: 400 12px / 1.7 "IBM Plex Mono", monospace;
  white-space: pre;
}

@media (max-width: 920px) {
  .element-facts {
    gap: 18px;
  }

  .element-reference {
    padding: 40px 0 52px;
  }

  .element-reference-layout {
    grid-template-columns: 1fr;
    gap: 20px;
  }
}

/* The rendered finish row. The swatches switch; this shows every finish at
   once, which is the only way to actually compare two fabrics. */
.element-finish-gallery {
  border-top: 1px solid var(--rule);
  padding: 44px 0 88px;
}

.element-finish-gallery h2 {
  margin: 0 0 14px;
  font: 400 32px / 1.06 "Instrument Serif", serif;
}

.element-finish-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 20px;
  margin-top: 22px;
}

.element-finish-tile {
  display: block;
  appearance: none;
  border: 0;
  background: none;
  cursor: pointer;
  padding: 0;
  text-align: left;
}

.element-finish-tile canvas {
  display: block;
  width: 100%;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  box-shadow: var(--shadow);
  opacity: 0;
  transition: opacity 220ms ease, border-color 160ms ease;
}

.element-finish-tile.is-lit canvas {
  opacity: 1;
}

.element-finish-tile:hover canvas {
  border-color: var(--soft);
}

.element-finish-tile[aria-pressed="true"] canvas {
  border-color: var(--accent);
}

.element-finish-tile span {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-top: 10px;
}

.element-finish-tile b {
  color: var(--ink);
  font: 500 11px / 1.3 "IBM Plex Mono", monospace;
}

.element-finish-tile em {
  color: var(--soft);
  font: 400 11px / 1.3 "IBM Plex Mono", monospace;
  font-style: normal;
}

/* ── The comparison ───────────────────────────────────────────────────────
   Two pictures of one object, at one size, adjacent. Everything below is in
   service of that sentence: equal columns so neither reads as the subject and
   the other as the footnote, one aspect ratio so the only difference on screen
   is the difference in the objects, and captions that say which is which
   because a stranger cannot tell a render from a photograph by looking, and
   should not have to guess. */
.element-compare {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
  align-items: start;
}

.element-compare-pane {
  margin: 0;
  min-width: 0;
}

.element-compare-pane .element-stage-live,
.element-compare-reference {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  box-shadow: var(--shadow);
}

.element-compare-reference {
  height: auto;
  object-fit: cover;
}

.element-compare-pane figcaption {
  margin-top: 10px;
  color: var(--soft);
  font: 400 12px / 1.5 "IBM Plex Sans", system-ui, sans-serif;
}

.element-compare-pane figcaption b {
  color: var(--ink-2);
  font-weight: 600;
}

@media (max-width: 700px) {
  .element-compare {
    grid-template-columns: 1fr;
  }
}

/* ── The review sheet ─────────────────────────────────────────────────────
   The same comparison, 196 times, plus the one number that says which rows
   deserve a second look. Rows rather than a grid: a grid puts two unrelated
   objects side by side and invites the eye to compare THOSE, which is the
   opposite of the job. */
.review-head {
  padding: 44px 0 32px;
}

.review-head h1 {
  margin: 8px 0 18px;
}

.review-head .element-lede + .element-lede {
  margin-top: 14px;
}

.review-summary {
  margin: 20px 0 5px;
  padding-left: 12px;
  border-left: 2px solid var(--accent);
  color: var(--ink-2);
  font: 500 10.5px / 1.5 "IBM Plex Mono", monospace;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.review-summary strong {
  color: var(--ink);
  font-weight: 600;
}

.filters-group {
  display: contents;
}

.review-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2) 0 96px;
}

.review-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(200px, 0.8fr);
  gap: var(--space-2);
  align-items: start;
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--rule);
}

.review-row[hidden] {
  display: none;
}

.review-pane {
  position: relative;
  display: grid;
  margin: 0;
  min-width: 0;
}

.review-pane img,
.review-pane canvas {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
  object-fit: cover;
}

/* A canvas nobody drew into is white, and white is indistinguishable from a
   pale object on a pale ground. It stays dim until it holds something. */
.review-pane-build canvas {
  opacity: 0;
  transition: opacity 200ms ease;
}

.review-pane-build.is-lit canvas {
  opacity: 1;
}

.review-row.is-void .review-pane-build::after {
  content: "builds nothing";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--fail);
  font: 500 11px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.review-pane figcaption {
  margin-top: 8px;
  color: var(--soft);
  font: 500 10px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.review-absent {
  display: grid;
  place-items: center;
  margin: 0;
  aspect-ratio: 1;
  border: 1px dashed var(--rule);
  border-radius: var(--radius);
  color: var(--soft);
  font: 500 11px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.review-meta h2 {
  margin: 0 0 8px;
  font-size: 1.15rem;
}

.review-meta h2 a {
  color: inherit;
}

.review-facts {
  margin: 10px 0 0;
  color: var(--soft);
  font: 500 10.5px / 1.5 "IBM Plex Mono", monospace;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* The parts line is quiet by default because most of what it reports is
   correct — a detached wall lantern is the boundary rule working. It raises
   its voice only past the gap where "in pieces" stops being a design and
   starts being a body that fell apart. */
.review-parts {
  margin: 6px 0 0;
  color: var(--ink-2);
  font: 500 10.5px / 1.5 "IBM Plex Mono", monospace;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.review-parts.is-loud {
  color: var(--fail);
}

/* A score is an audit instrument, not a pass badge: the number is paired with
   all five visible bands and the reason for the weakest one. */
.review-likeness {
  margin-top: 18px;
  padding-top: 12px;
  border-top: 1px solid var(--rule);
}

.review-like-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding-bottom: 7px;
  border-bottom: 1px solid var(--rule);
}

.review-like-head > span {
  color: var(--ink-2);
  font: 500 9px / 1 "IBM Plex Mono", monospace;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.review-like-head strong {
  color: var(--ink);
  font: 400 25px / 1 "Instrument Serif", serif;
  font-variant-numeric: tabular-nums;
}

.review-like-head small {
  color: var(--soft);
  font: 400 10.5px / 1 "IBM Plex Mono", monospace;
}

.review-bands {
  margin: 7px 0 0;
  padding: 0;
  list-style: none;
}

.review-bands li {
  display: grid;
  grid-template-columns: minmax(105px, 1fr) 24px;
  gap: 10px;
  align-items: center;
  padding: 3px 0;
  color: var(--ink-2);
  font: 400 10.5px / 1.35 "IBM Plex Mono", monospace;
}

.review-bandbar {
  display: flex;
  gap: 2px;
}

.review-bandbar i {
  width: 10px;
  height: 4px;
  border-radius: 1px;
  background: var(--rule);
}

.review-bands li[data-band="1"] .review-bandbar i:first-child {
  background: var(--accent);
}

.review-bands li[data-band="2"] .review-bandbar i {
  background: var(--pass);
}

.review-bands li[data-band="0"] .review-bandbar i {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--fail);
}

.review-likeness > p {
  margin: 9px 0 0;
  color: var(--soft);
  font: 400 12px / 1.5 "IBM Plex Sans", sans-serif;
}

.review-likeness > .review-provenance {
  padding-top: 7px;
  border-top: 1px solid var(--rule);
  font: 400 9px / 1.4 "IBM Plex Mono", monospace;
  letter-spacing: 0.03em;
}

/* Floorplan scores read as a compact grade stamp in the metadata column. The
   accent rule binds the score to the audit summary without turning it into a
   green/red release verdict; these are human similarity judgements. */
.review-element-grade {
  position: relative;
  padding-left: 12px;
}

.review-element-grade::before {
  content: "";
  position: absolute;
  top: 12px;
  bottom: 0;
  left: 0;
  width: 2px;
  background: var(--accent);
}

.review-element-grade.is-na::before {
  background: repeating-linear-gradient(
    to bottom,
    var(--soft) 0 4px,
    transparent 4px 7px
  );
}

.review-element-grade.is-na .review-like-head strong {
  font-size: 20px;
  letter-spacing: 0.02em;
}

.review-element-grade.is-na .review-like-head small {
  max-width: 90px;
  line-height: 1.2;
}

@media (max-width: 920px) {
  .review-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }

  .review-meta {
    grid-column: 1 / -1;
  }
}

/* The examples review sheet — the same row as /elements/review/, at the other
   aspect. An element is compared against a square reference tile; an example is
   compared against a drawing, and the drawing is 3:2. Both halves take the same
   box so the pair is one camera answered twice and nothing but the answer
   changes between them. */
.review-examples .review-row {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(180px, 0.55fr);
}

.review-examples .review-pane img,
.review-examples .review-absent {
  aspect-ratio: 3 / 2;
}

/* The cover arrives as inline SVG rather than an <img>, so it needs the frame
   the photograph gets from the rule above. */
.review-examples .review-pane svg {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  background: var(--sheet);
}

.review-compiled-frame {
  overflow: hidden;
  aspect-ratio: 3 / 2;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sheet);
}

.review-compiled-frame.is-top svg {
  transform: scale(1.45);
  transform-origin: center;
}

.gallery-review-note {
  max-width: 62ch;
  margin: var(--space-2) 0 0;
}

@media (max-width: 860px) {
  .review-examples .review-row {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* A format the host will not serve says so where its download button was.
   Not a disabled button — there is nothing to enable — and not a silent gap. */
.model-deliverable-absent {
  color: var(--soft);
  font: 500 11px / 1.4 "IBM Plex Mono", monospace;
  letter-spacing: 0.04em;
}

/* ── /materials ───────────────────────────────────────────────────────────
   One card is one SUBSTANCE — wood, tile, weave, brushed metal — because that
   is the word a person reaches for, and because 254 registered materials over
   twenty substances made a grid of near-duplicates rather than a vocabulary.
   Colour is not part of a card's identity; it is the parameter the detail page
   hands the reader a control for.

   Every swatch is the same real-world patch (see SWATCH_PATCH in
   scripts/build-site-html.mjs), so an 80 mm weave and a 900 mm parquet block
   are drawn at their true relative scale rather than each tiled to fit. */
.material-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
  gap: 30px 22px;
}

.material-card {
  display: block;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

.material-card[hidden] {
  display: none;
}

/* The pattern multiplies over the colour because that is what the shader does:
   a texture modulates albedo rather than replacing it. The colour arrives as
   `--swatch` so the detail page can repaint it with one assignment — the same
   shape the drawing set uses for `--bimtex-fill`. */
.material-swatch {
  background-color: var(--swatch, #d5d7d7);
  aspect-ratio: 4 / 3;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  background-repeat: repeat;
  background-blend-mode: multiply;
  transition: border-color 160ms ease;
}

.material-card:hover .material-swatch {
  border-color: var(--accent);
}

/* The rendered plate sits over the flat swatch rather than replacing it. The
   CSS tile is what a reader sees before three.js has loaded, on a machine with
   no WebGL, and behind a transparent material — glass at opacity .42 shows the
   colour through, which is the honest thing for it to show. */
.material-swatch,
.material-stage {
  position: relative;
  overflow: hidden;
}

.material-swatch canvas,
.material-stage canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 240ms ease;
}

.material-swatch canvas.is-lit,
.material-stage canvas.is-lit {
  opacity: 1;
}

/* Once the plate is rendered the card supplies the ground, not the material.
   Left on, the flat tile fills the frame around the sample and shows through
   a transparent one, so a pane of glass appears to be lying on more glass. */
.material-swatch.is-rendered,
.material-stage.is-rendered {
  background-image: none;
  background-color: transparent;
}

.material-body {
  padding-top: 11px;
}

.material-body h2 {
  margin: 0;
  font: 500 15px / 1.3 var(--display, inherit);
}

.material-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 3px 10px;
  margin: 5px 0 0;
  color: var(--soft);
  font: 400 11px / 1.4 "IBM Plex Mono", monospace;
}

.material-texture {
  color: var(--accent);
}

.material-soft {
  opacity: 0.62;
}

/* ── /materials/[substance] ─────────────────────────────────────────────── */
.material-detail {
  padding-bottom: var(--space-4);
}

.material-detail-head {
  padding-top: var(--space-3);
}

.material-detail-head h1 {
  margin: 6px 0 4px;
}

/* Swatch left, controls right — and the swatch is deliberately the larger of
   the two, because the control exists to change it and a reader watching a
   colour move needs enough of the colour to judge. */
.material-studio {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: 28px;
  align-items: start;
  padding-top: var(--space-2);
}

.material-stage {
  background-color: var(--swatch, #d5d7d7);
  aspect-ratio: 4 / 3;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  background-repeat: repeat;
  background-blend-mode: multiply;
  /* The one property the control writes. Everything above reads it. */
  transition: background-color 140ms ease;
}

.control-block + .control-block {
  margin-top: 22px;
}

.control-label {
  margin: 0 0 9px;
  color: var(--soft);
  font: 500 11px / 1.4 "IBM Plex Mono", monospace;
  letter-spacing: 0.04em;
}

.control-row {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}

.control-note {
  margin: 10px 0 0;
  color: var(--soft);
  font: 400 11px / 1.5 "IBM Plex Mono", monospace;
}

/* The ladder. Seven rungs, each showing the colour that step derives and how
   many registered materials already sit on it — which is the whole diagnostic:
   four names on one rung is four names for one material. */
.ladder-row {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 6px;
}

.ladder-row[hidden] {
  display: none;
}

.ladder-rung {
  display: grid;
  gap: 5px;
  justify-items: center;
  padding: 8px 4px;
  appearance: none;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: none;
  cursor: pointer;
  transition: border-color 140ms ease, background 140ms ease;
}

.ladder-rung:hover,
.ladder-rung:focus-visible {
  border-color: var(--accent);
}

.ladder-rung[aria-pressed="true"] {
  border-color: var(--ink, currentColor);
  background: color-mix(in srgb, currentColor 6%, transparent);
}

.ladder-chip {
  width: 100%;
  height: 26px;
  border: 1px solid var(--rule);
  border-radius: 3px;
}

.ladder-step {
  color: var(--soft);
  font: 500 10px / 1 "IBM Plex Mono", monospace;
}

.ladder-holders {
  color: var(--accent);
  font: 500 10px / 1 "IBM Plex Mono", monospace;
}

.element-copy-inline {
  margin-top: 0;
  padding: 5px 9px;
  font-size: 10px;
}

/* Authored hexes, kept visibly apart from the ladder. An accent IS its hex, so
   no value step reaches it, and drawing these as rungs would say the opposite
   of what the palette does. */
.material-authored,
.material-members {
  padding-top: var(--space-3);
}

.authored-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 12px;
}

.authored-chip {
  width: 30px;
  height: 30px;
  border: 1px solid var(--rule);
  border-radius: 4px;
}

.material-table {
  width: 100%;
  margin-top: 12px;
  border-collapse: collapse;
  font: 400 12px / 1.5 "IBM Plex Mono", monospace;
}

.material-table th {
  padding: 0 10px 8px 0;
  color: var(--soft);
  font-weight: 500;
  text-align: left;
}

.material-table td {
  padding: 7px 10px 7px 0;
  border-top: 1px solid var(--rule);
  vertical-align: middle;
}

.material-chip {
  display: inline-block;
  width: 15px;
  height: 15px;
  border: 1px solid var(--rule);
  border-radius: 3px;
}

.material-pager {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding-top: var(--space-3);
  font: 400 12px / 1 "IBM Plex Mono", monospace;
}

@media (max-width: 860px) {
  .material-studio {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 640px) {
  .material-grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 22px 14px;
  }

  .ladder-row {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .material-table {
    font-size: 11px;
  }
}

/* ============================================================================
   THE MASTHEAD — the one bar that spans both halves of the site.

   Loaded by the static builder (appended into dist/assets/site.css) and by the
   Next application (imported from app/global.css), so /docs and /playground
   wear the same header as the gallery and the examples. The markup comes from
   lib/site-nav.mjs; this file is the other half of the same rule, and neither
   stack keeps a copy of either.

   Two things make it portable. Fonts are read through the role tokens the Next
   app defines with next/font, falling back to the literal family names the
   static pages load from a stylesheet link — so one declaration resolves on
   both. And the container is fully specified here rather than leaning on the
   static site's global `.wrap`, which does not exist on the Next side.
   ========================================================================= */

.top {
  position: sticky;
  z-index: 30;
  top: 0;
  width: 100%;
  max-width: 100vw;
  border-bottom: 1px solid var(--rule);
  background: rgba(250, 248, 243, 0.9);
  backdrop-filter: blur(10px);
}

.top .wrap {
  display: flex;
  width: min(100vw, var(--wrap, 1200px));
  min-height: 56px;
  align-items: center;
  margin: 0 auto;
  padding-inline: 28px;
  gap: 28px;
}

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

.brand-mark {
  display: block;
  width: 34px;
  height: 34px;
  flex: none;
}

.brand-word {
  font: 400 23px / 1 var(--font-display-role, "Instrument Serif", Georgia, serif);
  letter-spacing: 0.005em;
}

.brand em {
  color: var(--accent);
  font-style: italic;
}

.top nav {
  display: flex;
  align-items: center;
  gap: 22px;
  margin-left: auto;
  color: var(--ink-2);
  font: 500 11px / 1 var(--font-utility-role, "IBM Plex Mono", monospace);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.top nav a {
  border-bottom: 1px solid transparent;
  padding-block: 6px;
  color: inherit;
  text-decoration: none;
  transition: border-color 160ms ease, color 160ms ease;
}

.top nav a:hover,
.top nav a[aria-current="page"] {
  border-color: var(--accent);
  color: var(--ink);
}

/* The React caller injects the shared markup into this span, so it must not
   become a box: `display: contents` lets the brand and the nav sit in the bar's
   flex row exactly as they do when a generator writes them there directly. */
.masthead-shared {
  display: contents;
}

.masthead-own {
  display: inline-flex;
  align-items: center;
}

/* /docs only, and only where the sidebar is not already on screen. */
.masthead-tree-toggle {
  display: none;
  margin-inline-start: 14px;
  border-radius: var(--radius, 2px);
  padding: 6px;
  color: var(--ink-2);
}

.masthead-tree-toggle:hover {
  color: var(--ink);
}

@media (max-width: 700px) {
  .top .wrap {
    padding-inline: 18px;
    gap: 16px;
  }

  .brand {
    gap: 7px;
  }

  .brand-mark {
    width: 30px;
    height: 30px;
  }

  .brand-word {
    font-size: 21px;
  }

  .top nav .nav-context,
  .top nav .nav-github {
    display: none;
  }

  /* Four global links do not fit beside the brand on a 375px screen, and until
     the bar was shared this went unnoticed: the overflow widened the document
     instead of the nav, so every page on the site scrolled sideways by ~25px
     and nothing looked broken enough to chase. The links scroll within the bar
     now, and `min-width: 0` is what stops a flex item from refusing to shrink
     below its content and pushing the page wide again. */
  .top nav {
    gap: 14px;
    min-width: 0;
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scrollbar-width: none;
  }

  .top nav::-webkit-scrollbar {
    display: none;
  }

  .top nav a {
    flex: none;
  }
}

/* Fumadocs reveals the docs sidebar at its `md` breakpoint; below it, this is
   the only way in. The number is theirs, so it is written once, here. */
@media (max-width: 767px) {
  .masthead-tree-toggle {
    display: inline-flex;
  }
}
