:root {
  --ink: #1a1a1a;
  --paper: #fafaf8;
  --line: #e4e2dc;
  --accent: #b08d57;
  --muted: #6b6b66;
  --max: 1100px;
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  color: var(--ink);
  background: var(--paper);
  line-height: 1.5;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

/* ---- Overflow safety net (2026-08-22) ----
   Two belt-and-braces rules so a phone screen can never be forced wider than
   its own width, whatever gets added to the page later:
   1. Images/media never exceed their container.
   2. Grid/flex children default to `min-width: auto`, which lets a wide
      descendant (e.g. a data table) force its track wider than the track's
      share of the row — a "grid blowout". Resetting to `min-width: 0` lets
      the child shrink and wrap/scroll internally instead of pushing the
      whole page wider than the viewport. */
img, svg, video { max-width: 100%; height: auto; }
.grid-3 > *, .grid-4 > *, .split > *, .foot-grid > *, .head-in > *,
.product-card > * { min-width: 0; }
.skip { position: absolute; left: -999px; top: 0; }
.skip:focus { left: 1rem; top: 1rem; background: #fff; padding: .5rem 1rem; z-index: 100; }

.wrap { max-width: var(--max); margin: 0 auto; padding: 0 1.5rem; }

.site-head {
  position: sticky; top: 0; z-index: 20;
  border-bottom: 1px solid var(--line);
  background: rgba(250, 250, 248, .82);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  backdrop-filter: saturate(180%) blur(14px);
}
/* The horizontal padding must be restated here, not left to `.wrap`. This
   element carries BOTH classes (`<div class="wrap head-in">`); `.wrap` sets
   `padding: 0 1.5rem` but `.head-in` is declared later at equal specificity, so
   its shorthand won and zeroed the side gutters. Invisible on desktop, where
   `.wrap`'s 1100px max-width centres the row anyway — but on a phone the
   monogram sat hard against the left screen edge and "Contact" against the
   right. Keep the 1.5rem in sync with `.wrap`. */
/* Vertical padding is 1.75rem, not the original 1.1rem (Charles, 2026-08-22:
   "give some padding space for the header, too edgy"). At 1.1rem the 3rem
   monogram left only ~18px of air above and below itself inside an 82px bar,
   so the mark read as pressed against the top of the window and against the
   header's own bottom rule. 1.75rem puts ~28px around it (bar ~104px) and
   keeps the mark optically centred in the band. The SIDE padding deliberately
   stays 1.5rem: it is what lines the monogram up with the hero text and every
   `.wrap` below it, so widening it would fix the top and break the left. */
.head-in { display: flex; align-items: center; justify-content: space-between; padding: 1.75rem 1.5rem; }
.brand { display: flex; align-items: center; gap: .6rem; text-decoration: none; color: var(--ink); }
/* Richie's monogram, inlined by forma_monogram() so it inherits `color`.
   Sized by height only — the artwork is taller than it is wide (361x451), so a
   fixed width would squash it. `width:auto` overrides the global
   `img, svg { max-width:100% }` rule above, which otherwise forces height:auto
   and collapses an inline SVG that has no intrinsic pixel size. */
/* 3rem is a floor, not a taste call. The bars are 13.33 units on a 451-unit
   canvas — 2.96% of the height — so at a 30px mark each bar lands on ~0.9 CSS
   px and antialiasing greys the whole lattice into a smudge on a 1x desktop
   display. Rendered at 30/40/48/56/64px, it only reads as woven bars from
   ~48px (3rem) up. Phones get 2.4rem because their 2-3x pixel ratio puts each
   bar back above one device pixel at that size. */
.brand-mark { height: 3rem; width: auto; flex: 0 0 auto; display: block; }
@media (max-width: 700px) { .brand-mark { height: 2.4rem; } }
/* Wordmark sits beside the monogram, never alone — Richie's brief was
   "with the word FORMA ... this is monogram, like LV". Tracking is opened up
   from .04em so the letters carry the same air as the lattice next to them. */
.brand-text { font-size: 1.3rem; letter-spacing: .14em; font-weight: 600; text-transform: uppercase; }
@media (max-width: 700px) { .brand-text { font-size: 1.1rem; letter-spacing: .1em; } }
.nav { display: flex; gap: 1.5rem; }
.nav a { color: var(--ink); text-decoration: none; font-size: .95rem; white-space: nowrap; }
/* Pre-existing, not caused by the logo: at 360px the brand + three nav links
   filled the row exactly, so "Contact" sat hard against the 1.5rem gutter with
   no visible right margin. Tightening the nav on phones gives the gutter back.
   The page never overflowed (scrollWidth == clientWidth at 360 and 390) — this
   is breathing room, not an overflow fix. */
@media (max-width: 700px) {
  /* Phones get 1.35rem rather than the desktop 1.75rem: the mark is 2.4rem
     here, and the header is sticky, so a ~94px bar would eat a tenth of a
     phone screen on every scroll. 1.35rem still clears the old 1.1rem. */
  .head-in { gap: .75rem; padding-top: 1.35rem; padding-bottom: 1.35rem; }
  .nav { gap: .65rem; }
  .nav a { font-size: .82rem; }
}
/* 2026-08-31: nav went from 3 links to 4 (Warranty added). The comment
   above already recorded that 3 links + the brand filled a 360-390px row
   "exactly", zero margin to spare — `body { overflow-x: hidden; }` (top of
   this file) was silently clipping the real overflow rather than showing a
   scrollbar, so scrollWidth==clientWidth never caught it. A 4th link pushed
   "Contact" partly past the viewport edge, screenshotted and confirmed at
   390px. Shrunk gap 1.05rem->.65rem and font .875rem->.82rem (desktop nav
   is untouched) — re-screenshotted at 390px and 360px clean, both with
   visible right margin.

   320px (iPhone 5/SE 1st-gen width, and the common responsive-design floor
   more generally) still failed after that shrink — and not with a visible
   scrollbar. `.head-in > * { min-width: 0; }` (top of this file, predates
   Warranty, added 2026-08-22 as a grid/flex-blowout safety net) lets
   flexbox shrink the `.brand` and `.nav` BOXES below their own children's
   content size instead of wrapping — the children (`.brand-mark`,
   `.brand-text`, each `<a>`) have no min-width:0 of their own, so they
   render at full natural size and paint past their shrunk parent box.
   `overflow-x:hidden` hides it from the viewport edge and `scrollWidth`
   never sees it, because the overflow never reaches the document edge —
   only the header's own internal elements collide. Measured (CDP
   getBoundingClientRect, see extract-20260830/scratch/nav-fix-m/ for the
   script and the numbers): at 320px this produced a real -5.2px glyph
   overlap between "FORMA" and "About" — brand-text's right edge landed at
   137.63px, "About"'s left edge at 132.42px. A first attempt at this fix
   shrunk the 340px-tier font/gap further, which was rejected on review:
   it chases the same failure mode with no margin to spare, and it does
   nothing about the underlying min-width:0 masking — any future change to
   the four link labels (or a 5th link) can silently reopen this exact bug
   without scrollWidth ever catching it.

   Fix: wrap the nav onto its own row below the brand instead. `.nav`'s
   natural width at the SAME comfortable 341-700px sizing above
   (gap:.65rem, font:.82rem — deliberately not shrunk further) is 215.05px
   at 320px, against 272px available on its own row: ~57px to spare, no
   squeeze left to hide anything. Net effect: legibility at this width is
   RESTORED to the 360/390 level, not degraded further. Verified 320/360/
   390/1280 via dp-screenshot.sh (droplet, never local Chrome) — 360/390/
   desktop CSS is untouched by this block, so their rendering is unchanged
   by construction. Header height at 320px (CDP getBoundingClientRect on
   `.site-head`): 82.58px (1 row) -> 114.27px (2 rows), +31.69px — the
   header does get taller on this narrowest band; see NOTES.md for why
   that trade-off was accepted rather than shrinking type past this point.

   Two alternatives considered and rejected: (1) hiding the "FORMA"
   wordmark and keeping only the monogram at this width — rejected because
   `.brand-text`'s own comment above records Richie's brief verbatim,
   "with the word FORMA ... this is monogram, like LV" — the mark and word
   are a deliberate pair, never one alone, at any width; (2) a hamburger/
   menu control — rejected as disproportionate: it needs JS + ARIA state
   this site otherwise avoids (the products filter bar's <noscript>
   fallback is built to work without JS) for one narrow band that a
   pure-CSS wrap already fully resolves.

   KNOWN, NOT FIXED (at the time this comment was written): the same
   min-width:0 mechanism left 360px with only about +2px of real slack, and
   390px about +11.6px — both read as visually clean, but neither had room
   left. That is now fixed structurally; see the block immediately below,
   which supersedes the 340px-only media query this comment described. */

/* ---------------------------------------------------------------------------
   HEADER OVERLAP, FIXED AT THE MECHANISM RATHER THAN AT ONE BREAKPOINT
   (2026-09-05, when a 5th header item — the quote CTA — had to be added).

   The previous fix wrapped the nav onto its own row only below 340px. It was
   correct for the bug in front of it, and it left the note above: 360px had
   about 2px of slack, so the NEXT nav item would silently reopen the same
   overlap at a width nobody was testing. That next item is here, so the
   breakpoint patch is replaced by a fix to the cause.

   The cause is `.head-in > * { min-width: 0; }` at the top of this file (a
   sound global guard against grid blowout) meeting a non-wrapping flex row:
   `.brand` and `.nav` are allowed to shrink below their own children's
   content size, the children do not shrink with them, so they paint past
   their box and collide. `scrollWidth == clientWidth` never catches it,
   because nothing ever reaches the document edge — the header's own elements
   overlap each other inside it.

   Three rules remove the possibility rather than the symptom:
     1. `.head-in` wraps at EVERY width, not just below 340px. When brand and
        nav no longer fit on one row, the nav takes its own row. Above ~700px
        there is enough room that this never triggers, so desktop is unchanged.
     2. `.brand` opts back out of the global `min-width: 0`. The monogram and
        the FORMA wordmark are a fixed lockup that must never be squeezed —
        `.brand-text`'s own comment records Richie's brief, "with the word
        FORMA ... this is monogram, like LV". It wraps or it moves; it does
        not shrink.
     3. `.nav` keeps `min-width: 0` and gains `flex-wrap` of its own, so when
        it IS squeezed the links reflow onto a second line instead of the box
        shrinking under stationary children. row-gap keeps the two lines
        legible.

   Net effect: a 6th, 7th or longer-worded nav item can no longer reopen this
   bug at any width. It costs header height on a narrow phone, which was
   already the accepted trade-off in the 320px fix above. */
.head-in { flex-wrap: wrap; }
.brand   { min-width: max-content; flex: 0 0 auto; }
.nav     { flex-wrap: wrap; justify-content: flex-end; row-gap: .45rem; }
.nav a:hover { color: var(--accent); }

/* The quote call to action. A nav link, not a separate control, so it wraps
   with the rest of the nav and inherits the fix above. */
.nav .nav-cta {
  border: 1px solid var(--ink);
  border-radius: 999px;
  padding: .3rem .85rem;
  transition: background-color .15s ease, color .15s ease;
}
.nav .nav-cta:hover { background: var(--ink); color: #fff; }

.section { padding: 6.5rem 0; }
.section + .section { border-top: 1px solid var(--line); }
@media (max-width: 700px) { .section { padding: 3.75rem 0; } }
.hero { padding: 5rem 0 3rem; text-align: left; }
@media (max-width: 700px) { .hero { padding: 2.75rem 0 2rem; } }
.pill {
  display: inline-block; font-size: .75rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--muted); border: 1px solid var(--line); border-radius: 999px; padding: .25rem .75rem;
  margin-bottom: 1.25rem;
}
h1 {
  font-size: clamp(2.25rem, 1.35rem + 4vw, 4rem); line-height: 1.07; margin: 0 0 1.25rem;
  font-weight: 600; letter-spacing: -.025em;
}
h2 { font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2.1rem); font-weight: 600; margin: 0 0 1.25rem; letter-spacing: -.015em; }
h3 { font-size: 1.1rem; font-weight: 600; margin: 0 0 .75rem; letter-spacing: -.005em; }
.lede { font-size: clamp(1.1rem, 1rem + .5vw, 1.35rem); color: var(--muted); max-width: 38rem; line-height: 1.45; }
p { max-width: 40rem; }
.prose p { color: #333; }

.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2.5rem; margin-top: 3rem; }
@media (max-width: 800px) { .grid-3 { grid-template-columns: 1fr; gap: 2rem; } }
.card { border: 1px solid var(--line); border-radius: 10px; padding: 1.75rem; background: #fff; }

.band-dark { background: var(--ink); color: #fff; }
.band-dark .muted { color: #b8b8b3; }

.btn {
  display: inline-block; padding: .85rem 1.75rem; border-radius: 980px; text-decoration: none;
  font-weight: 590; font-size: .95rem; letter-spacing: -.005em; transition: opacity .2s ease, border-color .2s ease;
}
.btn-primary { background: var(--ink); color: #fff; border: 1.5px solid var(--ink); }
.btn-primary:hover { opacity: .8; }
.btn-ghost { background: transparent; color: var(--ink); border: 1.5px solid var(--line); }
.btn-ghost:hover { border-color: var(--ink); }

.site-foot { border-top: 1px solid var(--line); background: #f2f1ec; padding: 3rem 0 1.5rem; margin-top: 2rem; }
/* 2fr 1fr 1fr since 2026-09-05: the footer gained a third column ("Get in
   touch") when the site finally got a working contact route. Under 900px it
   drops the about blurb's extra width and runs the two link columns side by
   side; under 700px everything stacks, as before. */
.foot-grid { display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 2rem; }
@media (max-width: 900px) { .foot-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 700px) { .foot-grid { grid-template-columns: 1fr; } }
.foot-col ul { list-style: none; padding: 0; margin: 0; }
.foot-col li { margin-bottom: .5rem; font-size: .9rem; }
.foot-col a { color: var(--ink); text-decoration: none; }
.brand-foot { margin-bottom: 1rem; }
.disclosure { font-size: .85rem; color: var(--muted); }
.foot-legal { border-top: 1px solid var(--line); margin-top: 2rem; padding-top: 1.25rem; font-size: .78rem; color: var(--muted); }
.foot-legal a { color: inherit; text-decoration: none; }
.foot-legal a:hover { color: var(--ink); text-decoration: underline; }

form.contact-form { max-width: 32rem; margin-top: 2rem; }
form.contact-form label { display: block; font-size: .85rem; margin-bottom: .3rem; color: var(--muted); }
form.contact-form input, form.contact-form textarea {
  width: 100%; padding: .6rem .7rem; border: 1px solid var(--line); border-radius: 4px; margin-bottom: 1rem;
  font-family: inherit; font-size: .95rem;
}
form.contact-form button { border: 0; cursor: pointer; font-family: inherit; }

/* ===========================================================================
   FORMS, NOTICES AND THE QUOTE CONFIGURATOR  (2026-09-05)

   ⛔ The honeypot rule below is load-bearing anti-spam, not decoration. It
   must hide the field from PEOPLE without hiding it from BOTS — so it uses
   position/clip rather than `display:none` or `visibility:hidden`, both of
   which a competent form-filler skips precisely because a human could not
   have filled them. Deleting this rule does not "clean up an unused style";
   it puts a visible field labelled Company on the contact page and turns the
   spam check into a trap for real customers.
   =========================================================================== */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.req { color: var(--accent); }
.form-note, .field-hint { font-size: .85rem; color: var(--muted); line-height: 1.5; }
.field-hint { display: block; margin-top: .3rem; }
.form-actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 1.5rem; }
.inline-form { display: inline; }

.notice { border-radius: 8px; padding: 1.25rem 1.5rem; margin: 1.5rem 0; border: 1px solid var(--line); }
.notice h2 { margin-top: 0; }
.notice-ok  { background: #f4f7f2; border-color: #cfe0c4; }
.notice-err { background: #fdf5f3; border-color: #e9c9c0; }
.notice ul { margin: .5rem 0 0; padding-left: 1.2rem; }

/* Steps read as a numbered sequence even with every image blocked. */
.quote-step { border-top: 1px solid var(--line); padding-top: 2rem; margin-top: 2.5rem; }
.quote-step h2 { display: flex; align-items: center; gap: .75rem; font-size: 1.25rem; }
.step-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.9rem; height: 1.9rem; border-radius: 999px; flex: 0 0 auto;
  background: var(--line); color: var(--muted); font-size: .9rem; font-weight: 600;
}
.quote-step.is-current .step-n { background: var(--ink); color: #fff; }
.step-wait, .step-hint { color: var(--muted); font-size: .95rem; max-width: 42rem; }

.choice-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin-top: 1.25rem; }
.choice-grid-tight { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.choice {
  text-align: left; cursor: pointer; font-family: inherit; color: var(--ink);
  background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: 1.1rem 1.25rem;
  display: flex; flex-direction: column; gap: .35rem;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.choice:hover { border-color: var(--ink); }
.choice.is-on { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
.choice-name  { font-weight: 600; font-size: 1rem; }
.choice-blurb { font-size: .85rem; color: var(--muted); line-height: 1.45; }

.spec-form { margin-top: 1.25rem; max-width: 46rem; }
.field-row { display: flex; flex-wrap: wrap; gap: 1rem; }
/* min-width:0 here is correct and is NOT the header bug: these children are
   text inputs, which shrink and stay inside their box. Without it a flex row
   of inputs refuses to wrap on a phone. */
.field { flex: 1 1 12rem; min-width: 0; margin-bottom: 1rem; }
.field-narrow { flex: 0 1 7rem; }
.field label { display: block; font-size: .85rem; margin-bottom: .3rem; color: var(--muted); }
.field input, .field select, .field textarea {
  width: 100%; padding: .6rem .7rem; border: 1px solid var(--line); border-radius: 4px;
  font-family: inherit; font-size: .95rem; background: #fff; color: var(--ink);
}
.field-check { border: 1px solid var(--line); border-radius: 8px; padding: 1rem 1.25rem; margin: 0 0 1rem; }
.field-check legend { font-size: .85rem; color: var(--muted); padding: 0 .4rem; }
.check { display: inline-flex; align-items: center; gap: .45rem; margin: .35rem 1.25rem .35rem 0; font-size: .92rem; }
.spec-form button, .quote-basket button { cursor: pointer; font-family: inherit; border: 1.5px solid var(--line); background: #fff; }
.btn-quiet { background: transparent; border: 0; color: var(--muted); text-decoration: underline; padding: .5rem 0; font-size: .88rem; }

.quote-basket { background: #f2f1ec; border-radius: 10px; padding: 1.25rem 1.5rem; margin-top: 1.5rem; }
.quote-basket h2 { margin-top: 0; font-size: 1.05rem; }
.basket-list { list-style: none; margin: 0; padding: 0; }
.basket-list li {
  display: flex; flex-wrap: wrap; gap: .75rem; align-items: baseline; justify-content: space-between;
  padding: .6rem 0; border-top: 1px solid var(--line);
}
.basket-list li:first-child { border-top: 0; }
.basket-meta { display: block; font-size: .85rem; color: var(--muted); }

.cert-list { list-style: none; margin: 1rem 0 1.5rem; padding: 0; }
.cert-list li { padding: .9rem 0; border-top: 1px solid var(--line); }
.cert-product { display: block; font-weight: 600; margin-bottom: .25rem; }
.cert-text { display: block; color: var(--muted); font-size: .93rem; line-height: 1.55; }
.fineprint { font-size: .85rem; color: var(--muted); line-height: 1.55; }

.contact-direct { margin-top: 3.5rem; border-top: 1px solid var(--line); padding-top: 2rem; }
.contact-list { display: grid; grid-template-columns: 8rem 1fr; gap: .5rem 1rem; max-width: 32rem; }
.contact-list dt { color: var(--muted); font-size: .9rem; }
.contact-list dd { margin: 0; }

/* ---- Imagery: restrained, large, architectural — never cropped busily ---- */
.media-hero {
  margin-top: 3rem;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: 2px;
}
.media-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }

.split {
  display: grid; grid-template-columns: 1.1fr 1fr; gap: 3rem; align-items: center;
}
@media (max-width: 800px) { .split { grid-template-columns: 1fr; } }
.split figure { margin: 0; overflow: hidden; border-radius: 2px; }
.split img {
  width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; display: block;
}

.stock-note {
  font-size: .72rem; color: var(--muted); margin: .5rem 0 0; letter-spacing: .02em;
}

/* ---- Products page — quiet grid, no boxed "widget" tiles ---- */
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 2.5rem 1.75rem; margin-top: 2.5rem; }
@media (max-width: 900px) { .grid-4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .grid-4 { grid-template-columns: 1fr; gap: 1.75rem; } }
.tile { padding-top: 1.25rem; border-top: 1px solid var(--line); }
.tile .code {
  display: inline-block; font-size: .78rem; font-weight: 700; letter-spacing: .05em;
  color: var(--accent); text-transform: uppercase;
}
.tile h3 { margin: .4rem 0 .5rem; font-size: 1rem; }
.tile .small { font-size: .82rem; margin: .4rem 0 0; }

.thermal-badge {
  display: inline-block; font-size: .7rem; text-transform: uppercase; letter-spacing: .06em;
  padding: .2rem .6rem; border-radius: 999px; border: 1px solid var(--line); color: var(--muted);
  vertical-align: middle; margin-left: .4rem; font-weight: 500;
}

.product-card {
  display: grid; grid-template-columns: 300px 1fr; gap: 2.5rem; align-items: start;
  padding: 3rem 0 0; margin-top: 3rem; border-top: 1px solid var(--line);
}
.product-card:first-of-type { margin-top: 0; padding-top: 2.5rem; border-top: none; }
.product-card.no-image { grid-template-columns: 1fr; }
.product-card h3 { margin-bottom: .25rem; }
@media (max-width: 800px) { .product-card { grid-template-columns: 1fr; } }
.product-card img {
  width: 100%; height: auto; border-radius: 6px; display: block; object-fit: cover;
}
.product-card.landscape img { aspect-ratio: 4 / 3; }
.product-card.portrait img { aspect-ratio: 3 / 4; }

/* ---- Spec tables: quiet data — thin rules, generous rows, no zebra/heavy header ---- */
.spec-table {
  width: 100%; border-collapse: collapse; margin: 1.25rem 0; font-size: .9rem; max-width: none;
}
.spec-table th, .spec-table td {
  border: none; border-bottom: 1px solid var(--line); padding: .85rem .6rem; text-align: left;
}
.spec-table thead th { border-bottom: 1.5px solid var(--ink); }
.spec-table tbody tr:last-child td { border-bottom: none; }
.spec-table th {
  font-weight: 600; font-size: .72rem; text-transform: uppercase;
  letter-spacing: .04em; color: var(--muted);
}
/* Numeric-leaning columns read as a column of numbers when right-aligned. */
.spec-table th:nth-child(n+2), .spec-table td:nth-child(n+2) { text-align: right; font-variant-numeric: tabular-nums; }

/* At and below the same breakpoint the product card drops to one column
   (site.css .product-card, max-width: 800px), a 5-column spec table cannot
   fit a phone screen honestly (measured: forces a 365px minimum content
   width against ~248px available inside the card at 360px device width —
   2026-08-22). Restack each row into label/value pairs instead of letting
   the page scroll sideways.

   Labels come from `content: attr(data-th)` alone (2026-08-30) — every
   `<td>` already carries its own column's label in `data-th`
   (forma_data_table(), inc/products.php), so this one rule works for any
   column set a product declares, not just STORO's five. It used to be
   backed up (and, before 2026-08-30, actually OVERRIDDEN — higher
   specificity always wins regardless of source order) by five
   `td:nth-child(n)::before { content: "..." }` rules hard-coding STORO's
   own five labels by position; those assumed every spec table on the site
   had exactly these five columns in this order, which stopped being true
   once a product could declare its own `spec_columns`. Removed rather than
   left dormant, so a future column set can't silently collide with a label
   that no longer applies to it. Verified this is a no-op for STORO's
   existing five-column tables: the `data-th` values were already identical
   to these five hard-coded strings on every live page (attribute emitted
   unconditionally by forma_data_table() since before this change), proven
   by a live-page mobile screenshot before and after this edit rendering
   identically. */
@media (max-width: 800px) {
  .spec-table thead { display: none; }
  .spec-table, .spec-table tbody, .spec-table tr, .spec-table td { display: block; width: 100%; }
  .spec-table tr { padding: .6rem 0; border-bottom: 1px solid var(--line); }
  .spec-table tbody tr:last-child { border-bottom: none; }
  .spec-table td {
    display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
    padding: .3rem 0; border: none; text-align: right;
  }
  .spec-table td::before {
    content: attr(data-th); color: var(--muted); font-size: .74rem; text-transform: uppercase;
    letter-spacing: .03em; text-align: left; flex: 0 0 auto;
  }
  .spec-table td:nth-child(1) { text-align: right; }
}

.note-block {
  font-size: .85rem; color: #333; background: #f7f6f2; border-left: 3px solid var(--accent);
  padding: .6rem .9rem; margin-top: .75rem; max-width: none;
}

/* ---- Scroll-linked motion (2026-08-22) ----
   Charles, by voice: "when you scroll down, something will move... and when
   you scroll up, it will rewind back." That is scroll-LINKED animation, not
   scroll-triggered — the scroll position itself is the timeline, so it plays
   forward and backward in lockstep with the scrollbar. Native CSS
   `animation-timeline: view()` does exactly this, on the compositor, no JS.

   Every element below is fully visible / at rest by default. The animated
   (initially-hidden) state is ONLY applied inside the @supports + @media
   gate, so:
     - a browser without scroll-timeline support        -> static, correct
     - prefers-reduced-motion: reduce                    -> static, correct
       (the gate requires "no-preference"; reduce never matches it)
     - a current Chromium with no reduced-motion pref    -> animates
   Only `opacity` and `transform: translateY/scale` are animated — compositor
   -only, no forced layout — and never anything horizontal, so this cannot
   reopen the sideways-overflow bug fixed above (re-measured after adding
   this: still 0px scroll at 320/360, see NOTES.md). */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .hero h1, .hero .lede, .hero > p {
      animation: forma-rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 40%;
    }
    .hero .lede { animation-delay: .05s; }
    .hero > p { animation-delay: .1s; }
    .media-hero img {
      animation: forma-settle linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 55%;
    }
    /* `.pcard`/`.cat-card` join `.product-card` here so the new products grid
       and category thumbnails move on the same scroll timeline as everything
       else. `.pcard[hidden]` is display:none, so a filtered-out card is out of
       the flow entirely and never animates. */
    .product-card, .pcard, .cat-card {
      animation: forma-rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 35%;
    }
    .detail-media img {
      animation: forma-settle linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 55%;
    }
    .split figure img {
      animation: forma-settle linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 55%;
    }
  }
}
@keyframes forma-rise-in {
  from { opacity: 0; transform: translateY(28px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes forma-settle {
  from { transform: scale(1.1); }
  to   { transform: scale(1); }
}

/* ---- Supplier brands (added 2026-08-22, Charles: "show STORO brand, and we
   will show more brands in the near future too") ---------------------------
   Confident type display of who supplies the systems FORMA carries, plus (as
   of the same evening, Charles authorising by voice 23:05 SGT) the actual
   STORO wordmark — cropped from the supplied deck's title-slide graphic, see
   inc/products.php for exactly what was and wasn't included in the crop.
   Grid goes to 2 columns as soon as there is a 2nd brand and stacks to 1 on
   phones; with a single brand it renders as one wide, deliberately prominent
   plate. */
.section-eyebrow {
  display: block; font-size: .78rem; text-transform: uppercase; letter-spacing: .1em;
  color: var(--accent); font-weight: 700; margin-bottom: 1.5rem;
}
.brand-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.75rem; }
.brand-plate {
  border: 1px solid var(--line); border-radius: 10px; padding: 2rem 2.25rem; background: #fff;
}
.brand-plate-name {
  display: block; font-size: clamp(1.75rem, 1.4rem + 1.2vw, 2.4rem); font-weight: 700;
  letter-spacing: .01em; line-height: 1.1;
}
.brand-plate-native {
  display: block; font-size: 1.05rem; color: var(--muted); margin-top: .35rem; letter-spacing: .04em;
}
.brand-plate-desc { font-size: .95rem; color: #333; margin-top: 1rem; }
.brand-plate .small { margin-top: .75rem; }

/* Supplier logo badge — the source PNGs are pure white ink on a transparent
   field (built for a dark slide background), so on this site's light cards
   they need var(--ink) behind them or they render invisible, not a "white
   box": confirmed by compositing the asset onto #fff before shipping this
   (see NOTES.md). Deliberately small relative to .brand-mark (FORMA's own
   header logo, 3rem/2.4rem) so a supplier mark reads as "we carry this
   brand," not as a second FORMA-scale logo. --sm is for the per-product
   attribution line, which has far less room than the brand plate. */
.brand-logo {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--ink); border-radius: 6px; padding: .55rem 1rem; margin-bottom: 1rem;
}
.brand-logo img { display: block; height: 1.5rem; width: auto; }
.brand-logo--sm { padding: .4rem .75rem; margin: 0 0 .6rem; }
.brand-logo--sm img { height: 1.1rem; }

/* ===========================================================================
   Products area — category thumbnails, category filter, product grid and the
   individual product pages (added 2026-08-22 for Richie's four requests).
   =========================================================================== */

/* ---- Category thumbnails --------------------------------------------------
   Photo-led cards, one per category, sitting above the filterable grid. The
   photograph is the member system's own STORO shot — no stock or invented
   imagery is introduced here. */
.cat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.75rem; margin-top: 2.5rem; }
@media (max-width: 900px) { .cat-grid { grid-template-columns: 1fr; gap: 1.5rem; } }
.cat-grid > * { min-width: 0; }

.cat-card {
  display: block; text-decoration: none; color: var(--ink);
  border: 1px solid var(--line); border-radius: 10px; background: #fff; overflow: hidden;
  transition: border-color .2s ease, transform .2s ease;
}
.cat-card:hover { border-color: var(--ink); transform: translateY(-2px); }
.cat-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.cat-card.is-active { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }

.cat-thumb { display: block; aspect-ratio: 4 / 3; overflow: hidden; background: #f2f1ec; }
.cat-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cat-body { display: block; padding: 1.25rem 1.4rem 1.5rem; }
.cat-name { display: block; font-size: 1.15rem; font-weight: 600; letter-spacing: -.01em; }
.cat-count {
  display: block; font-size: .72rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--accent); font-weight: 700; margin-top: .3rem;
}
.cat-blurb { display: block; font-size: .88rem; color: var(--muted); margin-top: .6rem; line-height: 1.45; }

/* ---- Filter bar -----------------------------------------------------------
   The <select> is a real GET form. PHP applies ?category= server-side, so this
   works with JavaScript off; the Show button is inside <noscript> because with
   JS on the change handler filters in place and the button would be dead
   weight. */
.filter-bar {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 1.5rem; flex-wrap: wrap; margin-bottom: .5rem;
}
.filter-bar h2 { margin-bottom: 0; }
.filter-form { display: flex; align-items: center; gap: .6rem; }
.filter-form label {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
  font-weight: 600;
}
.filter-form select {
  font-family: inherit; font-size: .95rem; color: var(--ink); background: #fff;
  border: 1px solid var(--line); border-radius: 999px; padding: .6rem 2.4rem .6rem 1.1rem;
  appearance: none; -webkit-appearance: none; cursor: pointer; max-width: 100%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%236b6b66' stroke-width='1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 1rem center; background-size: 12px 8px;
  transition: border-color .2s ease;
}
.filter-form select:hover { border-color: var(--ink); }
.filter-form select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 600px) {
  .filter-bar { align-items: stretch; }
  .filter-form { width: 100%; }
  .filter-form select { flex: 1 1 auto; }
}
.filter-count { font-size: .82rem; color: var(--muted); margin: .9rem 0 0; }
.pgrid-empty { color: var(--muted); margin-top: 2rem; }

/* ---- Product grid ---------------------------------------------------------
   `[hidden]` sets display:none in the UA sheet, but ANY display declaration in
   an author sheet beats it — so a `.pcard { display: grid }` would leave
   filtered-out cards visible. The explicit `[hidden] { display: none }` below
   is what actually makes the filter work; do not drop it. */
.pgrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 2rem 1.5rem; margin-top: 2rem; }
@media (max-width: 1000px) { .pgrid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 800px)  { .pgrid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px)  { .pgrid { grid-template-columns: 1fr; gap: 1.75rem; } }
.pgrid > * { min-width: 0; }

.pcard {
  display: flex; flex-direction: column; text-decoration: none; color: var(--ink);
  border: 1px solid var(--line); border-radius: 10px; background: #fff; overflow: hidden;
  transition: border-color .2s ease, transform .2s ease;
}
.pcard[hidden] { display: none; }
.pcard:hover { border-color: var(--ink); transform: translateY(-2px); }
.pcard:hover .pcard-cta { color: var(--accent); }
.pcard:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.pcard-thumb { display: block; aspect-ratio: 4 / 3; overflow: hidden; background: #f2f1ec; }
.pcard-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Two systems (G26, G61) have no photograph in STORO's overview. Rather than
   borrow a picture of a different system or drop in stock imagery, they get a
   typographic tile carrying the system code. */
.pcard-noimg, .detail-noimg {
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(160deg, #f4f3ee 0%, #e9e7df 100%);
}
.noimg-code {
  font-size: 2rem; font-weight: 700; letter-spacing: .12em; color: #b6b3a8;
  text-transform: uppercase;
}
.detail-noimg .noimg-code { font-size: 3.5rem; }

/* Column flex + `margin-top:auto` on the CTA so "Specification →" sits on the
   same baseline across a row whether the system name wraps to one line or two
   (G11 "Casement / Top Hung / Fixed Window" wraps; its neighbours do not). */
.pcard-body { display: flex; flex-direction: column; align-items: flex-start; padding: 1.1rem 1.25rem 1.35rem; flex: 1 1 auto; }
.pcard-body .code {
  display: inline-block; font-size: .78rem; font-weight: 700; letter-spacing: .05em;
  color: var(--accent); text-transform: uppercase;
}
.pcard-name { display: block; font-size: 1rem; font-weight: 600; margin: .35rem 0 .55rem; }
.pcard-hi { display: block; font-size: .82rem; color: var(--muted); margin-top: .6rem; }
.pcard-cta {
  display: block; font-size: .82rem; font-weight: 600; margin-top: auto; padding-top: .9rem;
  color: var(--muted); transition: color .2s ease;
}

/* `.tile` is a plain block on the products index; as a link (product page
   "Also in this category") it needs the anchor defaults stripped. */
.tile-link { display: block; text-decoration: none; color: var(--ink); transition: border-color .2s ease; }
.tile-link:hover { border-top-color: var(--ink); }
.tile-link:hover h3 { color: var(--accent); }
.tile-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.tile-link .small { color: var(--muted); }

/* ---- Individual product page ---------------------------------------------- */
.crumbs { font-size: .82rem; color: var(--muted); margin-bottom: 1.75rem; display: flex; gap: .5rem; flex-wrap: wrap; }
.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--ink); }
.crumbs [aria-current="page"] { color: var(--ink); font-weight: 600; }

.detail-head { display: grid; grid-template-columns: 1.05fr 1fr; gap: 3rem; align-items: center; }
@media (max-width: 800px) { .detail-head { grid-template-columns: 1fr; gap: 2rem; } }
.detail-head > * { min-width: 0; }
.detail-head h1 { margin-top: .5rem; }
.code-lg {
  display: inline-block; font-size: .85rem; font-weight: 700; letter-spacing: .1em;
  color: var(--accent); text-transform: uppercase;
}
.detail-media { margin: 0; overflow: hidden; border-radius: 6px; }
.detail-media img { width: 100%; height: auto; object-fit: cover; display: block; }
.detail-media.landscape img { aspect-ratio: 4 / 3; }
.detail-media.portrait img  { aspect-ratio: 3 / 4; }
.detail-media.detail-noimg { aspect-ratio: 4 / 3; border-radius: 6px; }
.detail-media figcaption { padding-top: .4rem; }
.detail-specs { max-width: 46rem; }

.pager { display: flex; gap: 1rem; align-items: center; justify-content: space-between; margin-top: 3.5rem; flex-wrap: wrap; }
.pager .btn { font-size: .88rem; padding: .7rem 1.4rem; }

.btn-sm { font-size: .85rem; padding: .6rem 1.2rem; }
/* `.btn-ghost` borrows the page's ink colour, which is invisible on the dark
   band. These two put it back to white without a new button component. */
.band-dark .btn-ghost, .btn-on-dark.btn-ghost { color: #fff; border-color: #4a4a47; }
.band-dark .btn-ghost:hover, .btn-on-dark.btn-ghost:hover { border-color: #fff; }
.band-dark .btn-primary, .btn-on-dark.btn-primary { background: #fff; color: var(--ink); border-color: #fff; }
