/* ==========================================================================
   KECA U.S.A. — specification tool
   Viewer page layout + studio presentation.

   Depends on keca-tokens.css being linked FIRST.

   Design constraints held throughout this file:
     - square corners everywhere (--radius is 0)
     - monochrome; the only non-neutral pair is --green / --green-bg and it is
       reserved exclusively for a PASSING scale verification
     - no invented accent colour, no rounded cards, no drop shadows at rest
   ========================================================================== */

/* --------------------------------------------------------------------------
   Page frame
   -------------------------------------------------------------------------- */

/* Skip link. Parked off-screen rather than display:none so it stays in the tab
   order; index.html, plan.html and room.html all carry the same idiom. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 20;
  background: var(--surface);
  border: 1px solid var(--accent);
  padding: .7rem 1rem;
  font-family: var(--head-font);
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .14em;
  text-decoration: none;
  color: var(--paper);
}
.skip-link:focus { left: 8px; top: 8px; }

.site-head {
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}

.site-head__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: 62px;
  padding-block: .7rem;
  flex-wrap: wrap;
}

.site-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .2rem 1.15rem;
}

.brand {
  font-family: var(--head-font);
  font-weight: 600;
  font-size: .82rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--paper);
  white-space: nowrap;
}

.brand__sub {
  display: block;
  font-weight: 500;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .16em;
  color: var(--paper-muted);
  margin-top: .15rem;
}

/* THE WAY BACK TO kecausa.com (2026-09-21). This tool is also served inside the
   main site at kecausa.com/specifier/, and nothing in it led back out: the
   brand links to the product index and the nav only reaches the other tools.
   Same rule as index.html's inline block — quiet, underlined so it reads as a
   link, and a full 44px target on its own box (no overhanging pseudo-element,
   for the reason given under .site-nav .back-link below). */
.site-head__id {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0 .9rem;
}
.site-exit {
  display: inline-flex;
  align-items: center;
  min-height: var(--keca-tap-min, 44px);
  font-family: var(--head-font);
  font-weight: 500;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .08em;
  white-space: nowrap;
  color: var(--paper-muted);
  text-decoration: underline;
  text-decoration-color: var(--line);
  text-underline-offset: .35em;
}
.site-exit:hover { color: var(--paper); text-decoration-color: var(--accent); }
/* The hairline between "the website" and "this tool": a fixed-height
   pseudo-element, so it stays a short rule and not a 44px one. */
.site-exit::after {
  content: "";
  flex: 0 0 1px;
  width: 1px;
  height: 1.25rem;
  margin-left: .9rem;
  background: var(--line);
}
@media print { .site-exit { display: none !important; } }

.back-link {
  font-family: var(--head-font);
  font-weight: 500;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .16em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--paper-muted);
  border-bottom: 1px solid var(--line);
  padding-bottom: 2px;
  white-space: nowrap;
}
.back-link:hover { color: var(--paper); border-bottom-color: var(--accent); }

/* The link's own box is only ~21px tall — under the 24px WCAG 2.2 target
   minimum and awkward to hit with a thumb. This extends the HIT AREA to 44px
   without moving a single rendered pixel: the pseudo-element is invisible,
   absolutely positioned, and centred on the text. */
.back-link { position: relative; }
.back-link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}

/* Inside the header nav there are now four of these side by side, and on a
   phone they wrap onto two rows. An overhanging 44 px pseudo-element is fine
   for ONE link but not for a wrapping row: each pseudo reaches ~11 px past its
   own text, so two stacked rows would have their invisible hit areas overlap
   in the gap between them and the later link in the DOM would silently steal
   taps aimed at the one above it. Inside .site-nav the anchor therefore takes
   the target on its OWN box — the same min-height + inline-flex rule
   plan.css:824 and room.css use for the identical header nav — and the
   pseudo-element is switched off so nothing overhangs. */
.site-nav .back-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--keca-tap-min, 44px);
  /* border-bottom would now sit at the foot of a 44px box, a clear 11px below
     the words it is supposed to underline. text-decoration stays welded to the
     text at any box height. */
  border-bottom: 0;
  padding-bottom: 0;
  text-decoration: underline;
  text-decoration-color: var(--line);
  text-underline-offset: .35em;
}
.site-nav .back-link:hover { text-decoration-color: var(--accent); }
.site-nav .back-link::after { content: none; }

.site-foot {
  border-top: 1px solid var(--line);
  margin-top: 3rem;
  padding-block: 1.4rem 2.4rem;
  color: var(--paper-muted);
  font-size: .78rem;
}

/* --------------------------------------------------------------------------
   Two-column shell — stage left, specification right.
   -------------------------------------------------------------------------- */

.viewer {
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(320px, 1fr);
  gap: clamp(20px, 3vw, 40px);
  align-items: start;
  padding-block: clamp(20px, 3vw, 36px);
}

.stage-col { min-width: 0; }
.info-col  { min-width: 0; }

/* --------------------------------------------------------------------------
   The studio stage.

   Every model in this library is untextured flat grey with a single material.
   The backdrop does the work that a texture would otherwise do: a soft
   vertical fall-off plus a radial floor pool that grounds the piece so it
   reads as a product sitting on a surface, not a blob floating in a void.
   -------------------------------------------------------------------------- */

.stage {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  border: 1px solid var(--card-line);
  border-radius: var(--radius);
  overflow: hidden;
  background-color: var(--surface);
  background-image:
    /* contact pool under the piece */
    radial-gradient(58% 24% at 50% 84%, rgba(28, 27, 27, .13), rgba(28, 27, 27, 0) 72%),
    /* horizon fall-off */
    linear-gradient(180deg, var(--surface) 0%, var(--surface) 40%, var(--tint) 100%);
}

/* Browsers without aspect-ratio still get a usable box. */
@supports not (aspect-ratio: 4 / 3) {
  .stage { height: 60vh; }
}

.stage__mv {
  width: 100%;
  height: 100%;
  display: block;
  background: transparent;
  opacity: 0;
  transition: opacity .35s ease;

  /* model-viewer CSS custom properties */
  --poster-color: transparent;
  --progress-bar-color: var(--accent);
  --progress-bar-height: 2px;
}
.stage__mv[data-ready="true"] { opacity: 1; }

/* We drive our own progress readout, so suppress the built-in bar by
   occupying its slot with an empty element. */
.mv-progress-slot { display: none; }

.stage__hint {
  margin: .6rem 0 0;
  font-size: .74rem;
  color: var(--paper-muted);
  letter-spacing: .02em;
}

/* --------------------------------------------------------------------------
   THE PRODUCT PHOTOGRAPH
   --------------------------------------------------------------------------
   Fills the stage in place of the 3D model for the 99 wood products that have
   one. Same box, same ground, same size on the page — a product with no model
   is not given a smaller or a shabbier presentation than one that has a model.

   `contain`, never `cover`. These are catalogue shots of furniture, and
   cropping a chair to fill a 4:3 box cuts the legs off the thing being
   specified. The stage's own gradient is the letterbox.

   Padding keeps the piece off the edge of the frame the way the orbit camera
   frames a model, so switching between a Tilia product and a wood one does not
   feel like switching between two different tools.
   -------------------------------------------------------------------------- */
.stage__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: clamp(10px, 3vw, 28px);
  /* The stage paints the ground; the photograph must not paint over it. */
  background: transparent;
}

/* --------------------------------------------------------------------------
   PHOTOGRAPH STRIP
   --------------------------------------------------------------------------
   Square corners and a 1 px rule, like every other control in this app. The
   selected thumbnail is marked by a solid --accent border AND by aria-pressed,
   never by border alone: on a monochrome page a one-shade border difference is
   not a reliable signal, and a designer sorting through six near-identical
   photographs of the same chair needs to know which one is on the stage.
   -------------------------------------------------------------------------- */
.photo-strip {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin-top: .6rem;
}
.photo-strip__btn {
  width: 68px;
  height: 68px;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  /* A 68 px control is comfortably over the 44 px touch minimum. */
}
.photo-strip__btn img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.photo-strip__btn[aria-pressed="true"] {
  border-color: var(--accent);
  border-width: 2px;
  padding: 2px;
}
.photo-strip__btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   NO 3D MODEL — the explanation and the routes that do work
   --------------------------------------------------------------------------
   Sits where the orbit hint sits, directly under the stage. Left-aligned
   prose under a rule: the house treatment for a statement of fact, and
   deliberately not the treatment used for the alert panels inside the stage.
   Nothing here is red, dashed, or shaped like a broken control.
   -------------------------------------------------------------------------- */
.nomodel {
  margin: .8rem 0 0;
  padding-top: .8rem;
  border-top: 1px solid var(--card-line);
}
.nomodel__label {
  margin: 0 0 .3rem;
  font-family: var(--head-font);
  font-weight: 600;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--paper);
}
.nomodel__body {
  margin: 0 0 .85rem;
  max-width: 62ch;
  font-size: .84rem;
  line-height: 1.65;
  color: var(--paper-soft);
}
.nomodel__routes {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
}
/* 44 px minimum so every one of these is a real touch target on a phone. */
.nomodel__routes .btn { min-height: 44px; }
@media (max-width: 767px) {
  /* Full width, stacked. Three buttons wrapping mid-label at 375 px read as a
     broken row rather than as three choices. */
  .nomodel__routes { display: grid; grid-template-columns: 1fr; }
  .nomodel__body { font-size: var(--keca-fs-base, 0.875rem); }
}

/* --------------------------------------------------------------------------
   True-dimension overlay — always drawn from the manifest, never from numbers
   measured off the mesh.
   -------------------------------------------------------------------------- */

.dim-overlay {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.6rem;
  padding: .6rem clamp(10px, 2vw, 16px);
  background: rgba(255, 255, 255, .92);
  border-top: 1px solid var(--card-line);
  font-size: .74rem;
  line-height: 1.5;
  color: var(--paper);
  backdrop-filter: saturate(120%) blur(2px);
}

.dim-overlay__item { display: flex; align-items: baseline; gap: .4rem; }

.dim-overlay__key {
  font-family: var(--head-font);
  font-weight: 500;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--paper-muted);
}

.dim-overlay__val { font-variant-numeric: tabular-nums; font-weight: 600; }
.dim-overlay__alt { color: var(--paper-muted); font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------------------
   Graceful state panels. Each sits over the stage, square and legible.
   -------------------------------------------------------------------------- */

.state {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* NOT justify-content:center. The stage clips its overflow, and a centred
     flex column whose content is taller than the box overflows equally off
     BOTH ends — the top of the panel becomes unreachable and is clipped away.
     On a short phone in landscape that would hide the install command, which
     is the one thing this panel exists to show. Auto margins centre exactly
     the same way when there IS spare room, and collapse to zero when there is
     not, leaving the panel scrollable from its true top. */
  justify-content: flex-start;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  gap: .55rem;
  text-align: center;
  padding: clamp(18px, 4vw, 40px);
  background: var(--surface);
  color: var(--paper);
}

.state > :first-child { margin-top: auto; }
.state > :last-child  { margin-bottom: auto; }

.state__eyebrow {
  font-family: var(--head-font);
  font-weight: 500;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--paper-muted);
}

.state__title {
  font-family: var(--head-font);
  font-weight: 600;
  font-size: .95rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  margin: 0;
}

.state__body {
  max-width: 46ch;
  margin: 0;
  font-size: .82rem;
  color: var(--paper-soft);
}

/* Appended to the loading panel by viewer.js when a download has been running
   for 30 visible seconds. Separated by a rule, not by a colour or an icon —
   it is a disclosure, not an error, and it must not read as one. */
.state__slow {
  border-top: 1px solid var(--card-line);
  padding-top: .7rem;
  margin-top: .3rem;
  color: var(--paper);
}

.state__detail {
  max-width: 52ch;
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .7rem;
  color: var(--paper-muted);
  /* diagnoseModelUrl() writes "<url>\n\n<explanation>". Without pre-wrap the
     blank line collapses and the URL runs straight into the sentence. */
  white-space: pre-wrap;
  /* break-all was shredding ordinary words mid-syllable ("arr ived",
     "alw ays"). anywhere breaks only tokens that genuinely cannot fit — which
     is the long URL, the thing that needed breaking in the first place. */
  word-break: normal;
  overflow-wrap: anywhere;
}

.state__fix {
  max-width: 52ch;
  margin: .4rem 0 0;
  padding: .6rem .75rem;
  border: 1px solid var(--card-line);
  background: var(--tint);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .7rem;
  text-align: left;
  color: var(--paper);
  overflow-x: auto;
  /* pre-wrap, not pre. The client is reading this panel on a phone; `pre` put
     the npm/cp command behind a horizontal scrollbar so the half that matters
     was off-screen. pre-wrap keeps the line breaks between the two commands
     and wraps the long one at its spaces, so it stays copy-pasteable. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Loading skeleton: a slow neutral sweep, disabled under reduced motion. */
.state--loading { background: var(--surface); }

.skeleton {
  width: min(62%, 320px);
  height: 8px;
  border: 1px solid var(--card-line);
  background: var(--tint);
  position: relative;
  overflow: hidden;
}
.skeleton__fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  background: var(--accent);
  transition: width .2s linear;
}

.state__pct {
  font-variant-numeric: tabular-nums;
  font-size: .72rem;
  color: var(--paper-muted);
}

/* --------------------------------------------------------------------------
   Scale badge + advisory notes
   -------------------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .28rem .6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  font-family: var(--head-font);
  font-weight: 500;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--paper);
}

/* The ONLY place colour is permitted: a passing scale verification.

   --green (#3E7C55) on --green-bg (#E7F0E4) is 4.26:1 — under the 4.5:1 that
   WCAG 1.4.3 AA requires for normal text, and this badge is .62rem/500, nowhere
   near the large-text exemption. It is also the badge that tells a designer the
   model's scale has been verified, i.e. the precondition for trusting AR at
   true size, so it was the single hardest thing to read on the page and the
   only thing rendered in colour.

   The green stays — it is the brand token and it still carries the meaning —
   but it moves to the border and the text drops to --paper (#1c1b1b) on the
   same tint: 14.0:1. Colour is now redundant with the wording rather than
   load-bearing, which WCAG 1.4.1 wants anyway. */
.badge--pass {
  border-color: var(--green);
  background: var(--green-bg);
  color: var(--paper);
}

/* Everything unverified stays monochrome — no invented warning hue. */
.badge--flag {
  border-color: var(--accent);
  background: var(--tint);
  color: var(--paper);
}

.badge--muted {
  border-color: var(--line);
  color: var(--paper-muted);
}

.notes { list-style: none; margin: 1rem 0 0; padding: 0; }

.note {
  border: 1px solid var(--card-line);
  border-left-width: 3px;
  border-left-color: var(--accent);
  background: var(--surface);
  padding: .7rem .85rem;
  margin-bottom: .5rem;
  font-size: .8rem;
  line-height: 1.6;
}
.note--ok { border-left-color: var(--green); }
.note--muted { border-left-color: var(--line); color: var(--paper-soft); }

.note__title {
  display: block;
  font-family: var(--head-font);
  font-weight: 500;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .16em;
  text-transform: uppercase;
  margin-bottom: .25rem;
}

/* --------------------------------------------------------------------------
   Specification blocks
   -------------------------------------------------------------------------- */

.panel {
  border: 1px solid var(--card-line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: clamp(14px, 2.2vw, 20px);
  margin-bottom: 1rem;
}

/* PROVENANCE OF THE GEOMETRY ON THE STAGE.

   Marked with the same 3 px left rule the notes use, so it reads as a
   qualification of the thing beside it rather than as another specification
   block. Monochrome: this palette has one accent, and inventing a warning
   colour for it would put a hue on the page that kecausa.com does not use.
   The weight comes from the tinted ground and the rule, not from red. */
.panel--caution {
  border-left-width: 3px;
  border-left-color: var(--accent);
  background: var(--tint);
}

.panel--caution .prov__body {
  margin: 0;
  font-size: .8rem;
  line-height: 1.65;
  max-width: 62ch;
}

.panel--caution .prov__body + .prov__body { margin-top: .55rem; }

.panel--caution .prov__meta {
  margin: .6rem 0 0;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .04em;
  color: var(--paper-muted);
}

@media (max-width: 767px) {
  .panel--caution .prov__body { font-size: var(--keca-fs-base, 0.875rem); }
}

.product-title { margin-bottom: .35rem; }

/* KECA's own published description. Body copy, not a specification value, so
   it is set as prose and kept clear of the spec lists below it. */
.product-desc {
  margin: .7rem 0 0;
  max-width: 62ch;
  font-size: .84rem;
  line-height: 1.7;
  color: var(--paper-soft);
}
@media (max-width: 767px) {
  .product-desc { font-size: var(--keca-fs-base, 0.875rem); }
}

.spec-list {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(9rem, auto) 1fr;
  gap: 0;
  font-size: .82rem;
}

.spec-list dt {
  font-family: var(--head-font);
  font-weight: 500;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--paper-muted);
  padding: .5rem .8rem .5rem 0;
  border-top: 1px solid var(--line);
  align-self: stretch;
}

.spec-list dd {
  margin: 0;
  padding: .5rem 0;
  border-top: 1px solid var(--line);
  font-variant-numeric: tabular-nums;
}

.spec-list dt:first-of-type,
.spec-list dd:first-of-type { border-top: 0; }

/* A manufacturer statement that runs to several lines — Tilia's materials
   paragraph — keeps its line breaks and is set as prose, not as a figure. */
.spec-list dd.prose {
  white-space: pre-line;
  font-variant-numeric: normal;
  line-height: 1.55;
}

/* A value that carries its source (a warranty and the page it was read from)
   says so with a dotted rule; the source is the title text. */
.spec-list dd.has-source { text-decoration: underline dotted var(--line); text-underline-offset: .2em; cursor: help; }

/* "Pending" is a first-class value, never a blank and never a guess. */
.pending {
  color: var(--paper-muted);
  font-style: normal;
  letter-spacing: .04em;
}
.pending::after {
  content: "";
}

/* THE THIRD STATE. "Not published by Tilia for the furniture itself." is a
   statement the manufacturer makes, not a figure nobody has supplied yet. It
   is set like a value, in the muted ink, and never in the Pending styling —
   the two must not be readable as the same thing. */
.notpub {
  color: var(--paper-muted);
  font-variant-numeric: normal;
}

/* The manufacturer's photograph beside a modelled product. */
.mfr-photo {
  margin: .9rem 0 0;
}
.mfr-photo img {
  display: block;
  width: 100%;
  max-width: 320px;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: contain;
  background: var(--tint);
  border: 1px solid var(--card-line);
}
.mfr-photo__cap {
  margin: .4rem 0 0;
  font-size: .74rem;
  line-height: 1.55;
  color: var(--paper-muted);
  max-width: 62ch;
}

/* --- options: one row per axis the manufacturer publishes --------------- */
.opt-list dd { font-variant-numeric: normal; }
.opt-values {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem .7rem;
}
.opt {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  font-size: .8rem;
}
/* The chip is the PUBLISHED colour value and nothing else — no border tint,
   no shading — inside a hairline so an ivory chip is visible on white. */
.opt__chip {
  display: inline-block;
  width: .85rem;
  height: .85rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  flex: 0 0 auto;
}

/* --- manufacturer data: features, usage areas, packaging --------------- */
.mfr-sub {
  margin: .9rem 0 .35rem;
  font-family: var(--head-font);
  font-weight: 500;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--paper-muted);
}
#mfr-body .mfr-sub:first-child { margin-top: 0; }
.taglist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: .3rem;
}
.taglist__item {
  font-size: .76rem;
  padding: .18rem .5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--paper);
}
.pkg-table {
  width: 100%;
  border-collapse: collapse;
  font-size: .76rem;
}
.pkg-table th,
.pkg-table td {
  text-align: left;
  vertical-align: top;
  padding: .35rem .5rem .35rem 0;
  border-top: 1px solid var(--line);
}
.pkg-table thead th {
  border-top: 0;
  font-family: var(--head-font);
  font-weight: 500;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--paper-muted);
  white-space: nowrap;
}
.pkg-table td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Wide on a phone: the table scrolls inside its own box, the page does not. */
.pkg-wrap { overflow-x: auto; }

/* Two figures that disagree, both printed. The note under them is the
   pipeline's own sentence about why. */
.dim-discrepancy__note {
  display: block;
  margin-top: .2rem;
  font-size: .74rem;
  line-height: 1.5;
  color: var(--paper-muted);
}

.dl-list { list-style: none; margin: 0; padding: 0; font-size: .82rem; }
.dl-list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: .5rem 0;
  border-top: 1px solid var(--line);
}
.dl-list li:first-child { border-top: 0; }
/* Grow the download link's tap target past the 24px minimum. The negative
   margin cancels the padding, so the row height and baseline are unchanged and
   the taller boxes still do not overlap each other. */
.dl-list a {
  font-weight: 600;
  display: inline-block;
  padding-block: .4rem;
  margin-block: -.4rem;
}

.format-note {
  margin: .8rem 0 0;
  font-size: .74rem;
  color: var(--paper-muted);
  line-height: 1.55;
}

/* --------------------------------------------------------------------------
   AR block
   -------------------------------------------------------------------------- */

.ar-block .btn { width: 100%; }

.ar-reason {
  margin: .6rem 0 0;
  font-size: .78rem;
  color: var(--paper-soft);
  line-height: 1.6;
}

/* --------------------------------------------------------------------------
   Phone layout. The client is reviewing this from a phone, so the stage keeps
   a usable height and the AR control sits full-width in the thumb zone.
   -------------------------------------------------------------------------- */

@media (max-width: 860px) {
  .viewer {
    grid-template-columns: 1fr;
    gap: 18px;
  }

  .stage {
    aspect-ratio: auto;
    height: 68vh;          /* fallback for browsers without dvh */
    height: min(68dvh, 560px);
    min-height: 300px;
  }

  .site-head__inner { min-height: 54px; }

  .spec-list { grid-template-columns: minmax(7.5rem, auto) 1fr; }

  .dim-overlay { gap: 0 1rem; font-size: .72rem; }
}

@media (max-width: 420px) {
  .dim-overlay { flex-direction: column; gap: .1rem; }
  .brand { font-size: .72rem; letter-spacing: .18em; }
}

/* --------------------------------------------------------------------------
   Reduced motion. viewer.js additionally refuses to enable model-viewer's
   auto-rotate when this query matches — CSS alone cannot stop it.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .stage__mv { opacity: 1; }
}

/* --------------------------------------------------------------------------
   Print — a designer printing the page should get the numbers, not the canvas.
   -------------------------------------------------------------------------- */

@media print {
  .stage, .ar-block, .back-link { display: none; }
  .viewer { grid-template-columns: 1fr; }
  .panel { border-color: #000; }
}

/* --------------------------------------------------------------------------
   State-gallery helpers (viewer-states.html only)
   -------------------------------------------------------------------------- */

.gallery { padding-block: clamp(20px, 3vw, 36px); }

.gallery__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: clamp(16px, 2.4vw, 28px);
}

.gallery__item { min-width: 0; }

.gallery__caption {
  font-family: var(--head-font);
  font-weight: 500;
  font-size: var(--keca-fs-micro, 0.6875rem);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--paper-muted);
  margin: 0 0 .5rem;
}

.gallery .stage { aspect-ratio: 4 / 3; height: auto; min-height: 0; }
.gallery .state { position: absolute; }

/* ==========================================================================
   PHONE TYPE FLOOR — measured on this page at 375 px
   --------------------------------------------------------------------------
   Everything below is a SENTENCE or a SPECIFICATION VALUE, not a label, and
   every one of them rendered under 14 px on a 375 px phone:

       .dim-overlay      11.8 px  the width/depth/height read off the stage
       .stage__hint      11.8 px
       .format-note      11.8 px  "millimetres are the source value"
       .state__detail    11.2 px  the diagnostic that says WHY a model failed
       .state__fix       11.2 px
       .ar-reason        12.5 px  why AR is unavailable
       .site-foot        12.5 px  "confirm against a physical sample"
       .note             12.8 px  the scale-verification verdict
       .spec-list dd     13.1 px  the specification values themselves
       .dl-list          13.1 px
       .state__body      13.1 px

   The uppercase micro-labels are a deliberate, documented step of the token
   scale (--keca-fs-micro, 11 px) and are NOT raised here — they are two or
   three tracked words, not running text. Sentences are a different problem:
   this page is read on a phone, on site, by someone deciding what to specify.

   Scoped to the single column so the desktop density is untouched, and using
   --keca-fs-base (14 px) rather than a hardcoded value.
   ========================================================================== */
@media (max-width: 767px) {
  .dim-overlay,
  .stage__hint,
  .format-note,
  .ar-reason,
  .site-foot,
  .note,
  .state__body,
  .dl-list,
  .spec-list { font-size: var(--keca-fs-base, 0.875rem); }

  /* Monospace runs visually smaller at the same size, and these two carry a
     URL and a shell command that someone has to read character by character. */
  .state__detail,
  .state__fix { font-size: var(--keca-fs-small, 0.75rem); }
}

/* --------------------------------------------------------------------------
   THE PRODUCT PICKER (2026-09-02). A native <select> plus two step buttons at
   the top of the specification column. 44 px tall so it is a thumb target;
   1rem so iOS Safari does not zoom the page on focus (same reasoning as the
   plan tool's inputs). Colours inherit the panel's ink; borders are the ink
   at low opacity so the control sits quietly under the product name.
   -------------------------------------------------------------------------- */
.product-nav {
  display: flex;
  gap: 0.4rem;
  align-items: stretch;
  margin: 0 0 0.9rem;
}
.product-nav__select,
.product-nav__step {
  font: inherit;
  font-size: 1rem;
  color: inherit;
  background: transparent;
  border: 1px solid var(--line);
  background: var(--surface);
  border-radius: 6px;
  min-height: 44px;
}
.product-nav__select {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.45rem 0.6rem;
}
.product-nav__step {
  flex: 0 0 44px;
  width: 44px;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.product-nav__step:disabled {
  opacity: 0.3;
  cursor: default;
}
.product-nav__select:focus-visible,
.product-nav__step:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}
