/*
  Sleep Essentials — base styles.
  Reset, type scale, section rhythm, shared components (buttons, focus
  ring, skip link). Later sections add rules to this file; they should
  not need to re-declare anything already established here.
*/

/* ---------- Reset ---------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  background-color: var(--paper);
  color: var(--ink);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: var(--step-0);
  line-height: 1.6;
}

h1, h2, h3, h4, h5, h6,
p, figure, blockquote, dl, dd {
  margin: 0;
}

ul, ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

img, svg {
  display: block;
  max-width: 100%;
  height: auto;
}

a {
  color: inherit;
}

/* Browsers italicize <address> by default; this page's only italic
   convention is the shot-list captions (below), so address text reads
   as plain body copy instead of borrowing that meaning. */
address {
  font-style: normal;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
}

fieldset {
  border: 0;
  margin: 0;
  padding: 0;
}

legend {
  padding: 0;
}

/* ---------- Type scale ---------- */

h1, h2 {
  line-height: 1.05;
  letter-spacing: -0.02em;
  font-weight: 700;
}

/* Exactly one h1 exists on the page (the hero). Every other section
   opens with an h2 at --step-2. `balance`
   respects the hero markup's own <br> breaks (it only smooths wrapping
   within each <br>-delimited segment, never across one) and was checked
   at both required widths: a no-op at 390px (the hero already wraps to
   enough lines that the browser's own balance line-count cap opts it
   out — harmless), a real fix at 1280px (the first segment's ragged
   "Sleep organic. Sleep without" / "fire retardants." became the far
   more even "Sleep organic. Sleep" / "without fire retardants."). */
h1 { font-size: var(--step-3); text-wrap: balance; }
h2 { font-size: var(--step-2); }

p {
  max-width: var(--measure);
}

.lede {
  font-size: var(--step-1);
  line-height: 1.35;
  color: var(--moss-700);
  /* Orphan control, not full balancing: a lede is prose (prose is never
     centered on this page and shouldn't be evened into uniform-width
     lines the way a short heading is) — `pretty` only keeps a lone word
     off the last line, `balance` would over-groom it. Unsupported
     browsers just ignore the declaration and keep normal wrapping. */
  text-wrap: pretty;
}

/* ---------- Layout / containers ---------- */

/* Full-bleed chrome and sections, content centered at --content-width
   without needing a wrapper div in every section's markup. */
header,
main > section,
footer {
  padding-inline: max(1.5rem, calc((100% - var(--content-width)) / 2));
}

/* ---------- Section rhythm ---------- */

main > section {
  padding-block: var(--section-pad-block);
}

/* A section left empty (if one is ever removed) must not
   leave a visible gap or break the alternating rhythm. The convention
   (see index.html) is to delete a cut section's <section> stub entirely
   — these two rules are a fallback for that, not the mechanism itself.

   :empty only matches a section with literally zero child nodes. A
   single whitespace character or an HTML comment left behind by someone
   cutting a section defeats it, and the section would render its full
   padding-block as a visible empty band. Kept as the baseline case. */
main > section:empty {
  display: none;
}

/* Belt-and-braces: :has() catches the more likely failure too. A
   section with no *element* children (comments and whitespace text
   nodes aren't elements) matches this even when it isn't :empty — e.g.
   <section id="firmness"><!-- removed --></section>. Written
   as its own rule (not merged into the :empty selector list above) so
   a browser without :has() support drops only this rule and still gets
   the :empty fallback. */
main > section:not(:has(> *)) {
  display: none;
}

/* Give each direct child of a section a default vertical rhythm so
   sections don't need to hand-roll spacing between heading/lede/etc. */
main > section > * + * {
  margin-top: 1em;
}

/* Ground alternation: --paper -> --sand-100 -> --paper ... Inverted
   (--moss-900) is used twice only: hero and education.
   --focus-ring is redefined here too (defaults to --moss-700 in
   tokens.css) — a --moss-700 ring is invisible on this ground, so the
   inherited custom property flips to --paper for every focusable
   descendant, keeping the outline rule itself singular. */
#hero,
#education {
  background-color: var(--moss-900);
  color: var(--paper);
  --focus-ring: var(--paper);
}

#hero .lede,
#education .lede {
  color: var(--paper);
}

#differentiators,
#authority,
#showroom {
  background-color: var(--sand-100);
}

#firmness,
#shotlist {
  background-color: var(--paper);
}

/* ---------- Header / footer chrome ---------- */

header {
  padding-block: 1.25rem;
  border-bottom: 1px solid var(--sand-200);
}

.site-wordmark {
  margin: 0;
  font-size: var(--step-0);
  font-weight: 700;
  letter-spacing: -0.01em;
}

footer {
  padding-block: 2.5rem;
  background-color: var(--sand-200);
  color: var(--ink);
}

/* The footer must not render as an empty colored bar if its content is
   ever removed. Same :empty / :has() belt-and-braces pair as sections
   above, for the same reason. */
footer:empty {
  display: none;
}

footer:not(:has(> *)) {
  display: none;
}

/* ---------- Skip link ---------- */

.skip-link {
  position: fixed;
  top: 1rem;
  left: 1rem;
  z-index: 10;
  background-color: var(--moss-900);
  color: var(--paper);
  padding: 0.75em 1.25em;
  border-radius: var(--radius);
  /* Self-relative offset (not a fixed rem value) so the hidden position
     always clears the element regardless of its computed height at the
     current step-0 clamp value. */
  transform: translateY(-200%);
}

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

/* ---------- Focus ---------- */

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

/* ---------- Buttons ---------- */
/* Markup shape: <a class="cta cta--primary"> / <a class="cta cta--secondary">.
   Two classes only — do not invent a third variant or a second radius. */

.cta {
  display: inline-block;
  padding: 0.75em 1.5em;
  border-radius: var(--radius);
  font-size: var(--step-0);
  font-weight: 600;
  text-decoration: none;
  border: 1px solid transparent;
}

.cta--primary {
  background-color: var(--moss-700);
  color: var(--paper);
}

.cta--primary:hover,
.cta--primary:focus-visible {
  background-color: var(--moss-900);
}

.cta--secondary {
  background-color: transparent;
  color: var(--moss-700);
  border-color: var(--moss-700);
}

.cta--secondary:hover,
.cta--secondary:focus-visible {
  background-color: var(--moss-700);
  color: var(--paper);
}

/* ---------- Hero ---------- */
/* The hero calls for a full-bleed macro of natural-latex cell structure
   behind the headline. No such photograph is available, so the depth
   comes from layered radial-gradients over the #hero ground instead.
   #hero's background-color and text color are already set globally
   above — this only adds background-image, never background-color or
   color. Every stop is an existing token's hex decomposed to rgb() so it
   can carry an alpha channel; no new hue is introduced, which keeps the
   palette closed. */

/* SWAP POINT: if a real latex-macro image is supplied, set it here.
   Keep the gradient as a fallback layer beneath it. */
/* #hero { background-image: url("../img/latex-macro.jpg"), <the five radial-gradients below, unchanged>; } */

#hero {
  background-image:
    radial-gradient(ellipse 55% 45% at 12% 8%, rgba(253, 252, 249, 0.09), transparent 60%),
    radial-gradient(ellipse 50% 60% at 92% 6%, rgba(67, 112, 84, 0.4), transparent 65%),
    radial-gradient(circle at 80% 58%, rgba(143, 176, 155, 0.16), transparent 45%),
    radial-gradient(circle at 22% 88%, rgba(67, 112, 84, 0.22), transparent 50%),
    radial-gradient(ellipse 85% 70% at 50% 118%, rgba(20, 32, 26, 0.65), transparent 70%);
  background-repeat: no-repeat;
  background-position: center;
}

.eyebrow {
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* --moss-300 is 2.31:1 on a light ground (fails AA) and only valid as
   text on --moss-900/--moss-700. Scoped to #hero rather than declared
   on the bare class so a later section author reusing .eyebrow on
   --sand-100 gets the light-ground default (inherited --ink) instead
   of silently shipping unreadable text — same reasoning as the
   ground-aware buttons and the focus-ring token above. */
#hero .eyebrow {
  color: var(--moss-300);
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
}

/* Both buttons are ground-aware.
   Same auto-correct pattern already used for .lede above: the shared
   component's color adapts per ground via a section-scoped selector,
   never a new class or a new variant.

   .cta--primary is --paper text on a --moss-700 fill — 10.03:1 on the
   light grounds it was validated for, but the FILL itself is only
   ~1.5:1 against the inverted #hero/#education ground (moss-700 on
   moss-900): the button shape nearly vanishes and the bordered
   secondary ends up reading stronger than the primary action. On
   inverted grounds the primary inverts too: --moss-900 text on a
   --paper fill (15.1:1). Hover/focus swap the fill to --moss-300
   (6.53:1 against --moss-900 text) instead of the light-ground hover's
   --moss-900 — reusing that value here would fill the button with the
   exact color of the page behind it and make it disappear on hover.

   .cta--secondary is --moss-700 text/border on transparent — 10.03:1 on
   light grounds, but only 1.51:1 (fails AA) against the inverted
   ground. Both figures confirmed by direct luminance calculation —
   pa11y cannot reliably sample contrast through a background-image
   ancestor and reported no issues on either before this fix. Every
   inverted section on this page sits on a gradient, so that blind spot
   applies wherever a later section styles content over #hero or
   #education, not just here. */
#hero .cta--primary,
#education .cta--primary {
  background-color: var(--paper);
  color: var(--moss-900);
}

#hero .cta--primary:hover,
#hero .cta--primary:focus-visible,
#education .cta--primary:hover,
#education .cta--primary:focus-visible {
  background-color: var(--moss-300);
  color: var(--moss-900);
}

#hero .cta--secondary,
#education .cta--secondary {
  color: var(--paper);
  border-color: var(--paper);
}

#hero .cta--secondary:hover,
#hero .cta--secondary:focus-visible,
#education .cta--secondary:hover,
#education .cta--secondary:focus-visible {
  background-color: var(--paper);
  color: var(--moss-700);
}

/* ---------- Differentiator strip ---------- */
/* No section-level heading — the three item titles are sibling <h2>s
   that stand in for one. Deliberately unornamented (no borders, no card
   treatment): a bare three-up that reads as a continuation of the
   hero. */

.diff-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(2rem, 5vw, 3rem);
}

/* Short headings are exactly what text-wrap: balance is for — it evens
   the line lengths within each heading's own column instead of leaving
   the last line however the default greedy wrap happens to break it.
   Does not touch column height/alignment between the three items.

   Checked at both required widths, not assumed: at 390px this alone
   fixed the first item cleanly (4 ragged lines -> 3 even ones). At
   1280px it initially made that same item WORSE (3 lines -> 4, with an
   orphaned one-word first line) — balance was choosing a break inside
   "fire-retardant" that a human never would, because the browser's own
   greedy wrap already treats the hyphen as a valid break point and
   balance inherits that same set of candidate breaks. Fixed at the
   presentation layer, not the copy: index.html wraps that one compound
   word in <span class="keep-together"> (below) instead of swapping in a
   non-breaking-hyphen entity, so the source text stays a plain,
   findable "fire-retardant" — confirmed this fixes both widths at once,
   to the same clean 3-line result, without which `balance` alone was a
   net regression at desktop. */
.diff-grid h2 {
  text-wrap: balance;
}

/* Suppresses line-break candidates inside a short run of text (e.g. a
   hyphenated compound) without altering the text itself, so a browser
   find-in-page match on the plain word still works. Reach for this
   instead of a non-breaking-hyphen entity when a heading needs a
   compound word kept on one line. */
.keep-together {
  white-space: nowrap;
}

.diff-grid > li > * + * {
  margin-top: 0.5em;
}

/* repeat(3, 1fr) is already
   content-aware — each column's floor is its own item's min-content, not
   an even three-way split — which is exactly why the three columns don't
   render equal-width above. The first item's floor is set by the
   .keep-together "fire-retardant" span above: it cannot break, so its
   rendered width is a hard lower bound on column 1 the same way any
   other single word would be, just a wider one. That's correct behavior,
   not the bug. The bug is the breakpoint below: 640px let three columns
   resume before there was room for all three floors (col 1's especially)
   plus two gaps to actually fit side by side — confirmed by sweep to
   overflow continuously from 641px to 721px. Raised to 750px, the same
   "measure the real combined minimum, then set the breakpoint past it
   with margin" fix already applied to .firmness-layout below (1000px
   against a measured ~930px need) and to .location above. */
@media (max-width: 750px) {
  .diff-grid {
    grid-template-columns: 1fr;
  }
}

/* ---------- Education: cutaway diagram ---------- */
/* Hand-authored, not generated.
   Inlined in index.html so it themes via currentColor/custom properties.
   Two column stacks share one scale (identical y-range, 70-290, in the
   SVG's own viewBox units) so the comparison is honest — see the
   in-diagram note. No hex value, no radius: every fill is an existing
   token via var(), and every rect has square corners (no rx).

   This is the most persuasive asset on the page and was originally
   rendering small and left-anchored (720px intrinsic width, unstretched,
   inside a ~1152px content column). Capped at a deliberate max-width
   bigger than the old intrinsic size, centered, and the svg itself
   stretches to fill that box — it now commands the section instead of
   reading as an afterthought, at every width up to the cap. Below the
   cap (all phone widths, and this cap itself well under the full content
   column) the existing max-width:100%/height:auto from the base reset
   still governs, so nothing regresses at 390px.

   This 60rem also sets the page's px-per-SVG-unit scale (60rem*16 / 720
   viewBox units): .firmness-figure derives its own max-width from this
   same value so the two diagrams render identically-sized layers — see
   the scale note over there. Change this number and that one
   drifts out of sync with it. */

#education figure {
  max-width: 60rem;
  margin-block: clamp(2rem, 5vw, 3rem);
  margin-inline: auto;
}

.cutaway {
  width: 100%;
  height: auto;
}

.cutaway__title {
  font-size: 34px;
  font-weight: 700;
  text-anchor: middle;
  fill: currentColor;
}

.cutaway__label {
  font-size: 27px;
  text-anchor: start;
  fill: currentColor;
}

/* De-emphasized, so it gets its own fill rather than inheriting
   currentColor like the title/label roles above. */
.cutaway__note {
  font-size: 26px;
  text-anchor: middle;
  fill: var(--moss-300);
}

/* The conventional stack's three light fills (paper,
   sand-100, sand-200) sit at roughly 1.1-1.3:1 against each other, so a
   currentColor divider at 30% opacity was nearly invisible between them
   — the four (now five) bands read as one undifferentiated block, which
   defeats the point of a stack a viewer needs to count. --ink is the
   darkest token on the page and reads clearly against every fill this
   diagram uses, light or green, so the internal seams between layers
   use it instead, at higher opacity and width. This does cost the true
   outer silhouette some crispness against the --moss-900 canvas (--ink
   is close to that ground, ~1.1:1) — .cutaway__outline below restores
   that separately, painted on top, so the two jobs (internal dividers,
   outer edge) don't fight over one stroke color. */
.cutaway__layer {
  stroke: var(--ink);
  stroke-opacity: 0.85;
  stroke-width: 3;
}

/* Painted after the fill rects (see markup order) so its currentColor
   (--paper) perimeter sits on top at the stack's true outer edge, where
   the --ink dividers above would otherwise all but disappear against
   the dark ground. Unfilled — it only draws the four outer edges of
   each stack's full bounding box, leaving the internal seams to the
   ink strokes on the individual layer rects underneath. */
.cutaway__outline {
  fill: none;
  stroke: currentColor;
  stroke-opacity: 0.6;
  stroke-width: 3;
}

.cutaway__swatch {
  stroke: currentColor;
  stroke-opacity: 0.3;
  stroke-width: 1;
}

.cutaway__fill--paper    { fill: var(--paper); }
.cutaway__fill--sand-100 { fill: var(--sand-100); }
.cutaway__fill--sand-200 { fill: var(--sand-200); }
.cutaway__fill--moss-300 { fill: var(--moss-300); }
.cutaway__fill--moss-500 { fill: var(--moss-500); }

/* ---------- Video links ---------- */
/* Plain text links inside #education share the ambient --paper text
   color, so color alone can't distinguish them from the surrounding
   prose — underline is the non-color affordance. */

.video-links {
  display: flex;
  flex-direction: column;
  gap: 0.5em;
}

.video-links a {
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ---------- Authority ---------- */
/* Portrait is a 525x720 frame grabbed from the company's own footage
   (identity of the presenter is unconfirmed,
   so no name appears anywhere in this section, in the caption or the
   copy around it). Held at modest scale (capped well under its intrinsic
   width by the grid track below) rather than blown up, because a
   1080p-sourced frame goes soft when enlarged. The duotone recolors it
   into the page's own two tokens so the modest size and flat treatment
   read as an editorial choice, not an unfinished asset. */

.authority-grid {
  display: grid;
  grid-template-columns: minmax(12rem, 16rem) 1fr;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
}

@media (max-width: 640px) {
  .authority-grid {
    grid-template-columns: 1fr;
  }

  .authority-portrait {
    max-width: 12rem;
    margin-inline: auto;
  }
}

/* Duotone: grayscale the photo first, then lay a --moss-900 -> --sand-100
   gradient over it with mix-blend-mode: color. That blend mode takes
   hue/saturation from the gradient but keeps luminosity from the layer
   underneath, so the photo's actual light/dark structure (the thing
   that makes it read as a photo and not a swatch) survives untouched —
   only its color changes. A first pass tried multiply(--moss-900) then
   screen(--sand-100) stacked as two flat layers, which is the standard
   print-duotone recipe, but --sand-100 sits so close to white that the
   screen pass overpowered the image at any tone and washed it out to
   near-blank regardless of what was under it — caught by rendering the
   page, not by reading the CSS. Both stops are var() references to
   tokens.css, so nothing here is a new hex value and a palette change
   recolors this for free. overflow: hidden clips the gradient layer to
   the frame's own radius. */
.authority-portrait {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--sand-200);
  border-radius: var(--radius);
}

.authority-portrait img {
  filter: grayscale(1) contrast(1.05);
}

.authority-portrait::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(160deg, var(--moss-900), var(--sand-100));
  mix-blend-mode: color;
  pointer-events: none;
}

/* Graceful degradation: mix-blend-mode support is ~98%, not 100%. Where
   it's unsupported the property is dropped silently and the gradient
   above composites at the default `normal` blend instead — fully
   opaque, completely hiding the photo underneath. That isn't a
   degraded duotone, it's a person's face replaced by a flat colored
   rectangle, which is the one failure mode this section can't afford.
   @supports not (...) targets exactly that gap and removes the overlay
   outright, leaving the plain grayscale photo — still legible, still
   on-thesis, visually checked before it ever reached this page — as
   the fallback instead of nothing. */
@supports not (mix-blend-mode: color) {
  .authority-portrait::after {
    display: none;
  }
}

.proof {
  display: flex;
  flex-direction: column;
  gap: 1em;
}

.proof li {
  padding-top: 1em;
  border-top: 1px solid var(--sand-200);
}

.proof li:first-child {
  padding-top: 0;
  border-top: 0;
}

.proof strong {
  display: block;
  font-size: var(--step-1);
  color: var(--moss-700);
}

/* ---------- Firmness ---------- */
/* Interactive build-your-own-firmness beat. The controls are
   real <input type="radio"> elements inside a <fieldset> (index.html) —
   no custom div-based control, so keyboard operation, focus handling, and
   screen-reader semantics come from the browser for free; the label wraps
   each input purely to widen the click/tap target.

   The layer proportions for all three firmness levels are pre-rendered as
   three <g class="firmness-layers"> groups at the same coordinates inside
   the SVG (index.html); firmness.js only toggles which one carries
   .is-active and swaps the caption text — it never computes or redraws
   geometry. That keeps the only thing that ever changes to opacity, which
   is in-bounds of this page's Motion rule (transitions only on
   color/background-color/border-color/opacity). The
   medium group already carries .is-active in the markup, so the section
   renders its default configuration with JavaScript disabled. */

/* Column 1 was originally capped at minmax(16rem, 22rem) — about 352px —
   while the three pills need roughly 365px at this width (tightened below
   from an earlier, even wider padding). That forced Firm onto its own row.
   Meanwhile column 2 was a bare 1fr, which stretched to fill whatever was
   left over even though the diagram itself never grows past 20rem, leaving
   a dead gap beside it.

   Fixed both by sizing each column to what it actually needs instead of
   guessing: column 1 is max-content (always exactly wide enough for the
   picker's own content, immune to future copy/padding changes); column 2
   matches .firmness-figure's own cap below, so the figure fills its track
   exactly instead of floating inside extra space. justify-content: center
   keeps the resulting (content-sized, not 72rem-wide) block centered in
   the section rather than stretched with a void in the middle.

   Column 2 was originally 20rem — that made
   this diagram render at 0.8px per SVG unit (20rem*16 / 400 viewBox units)
   against the #education cutaway's 1.333px per unit (60rem*16 / 720), so
   the "two diagrams share one scale" claim below in index.html was false:
   same title class rendering 27px here vs 45px there, same mattress stack
   88x176px here vs 147x293px there. Both stacks are drawn at byte-identical
   coordinates on purpose (see index.html) — that must not change — so the
   fix is the outer rendered size, not the artwork. calc(60rem * 400 / 720)
   scales this 400-unit viewBox by the exact same factor #education applies
   to its 720-unit one, so identical SVG units now render as identical
   pixels in both diagrams, not just an approximate match.

   That widens column 2 from 320px to ~533px, which pushes the two-column
   minimum from the old ~717px to ~930px (measured: max-content column 1
   ~365px + 2rem gap + ~533px column 2). Below 1000px the picker + diagram
   no longer both fit in one row at any viewport this page supports (the
   section's content width is viewport minus 48px of padding below
   ~1200px), so the layout collapses to a single, full-width column instead
   of letting a max-content track overflow its container. The breakpoint
   moves from 780px to 1000px to clear the new ~930px need with margin,
   verified by the same overflow sweep (390-1280px) that caught the old
   threshold. */
.firmness-layout {
  display: grid;
  grid-template-columns: max-content calc(60rem * 400 / 720);
  justify-content: center;
  gap: 2rem;
  align-items: start;
}

@media (max-width: 1000px) {
  .firmness-layout {
    grid-template-columns: 1fr;
    justify-content: normal;
  }
}

.firmness-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.firmness-picker legend {
  flex: 0 0 100%;
  margin-bottom: 0.75em;
  font-weight: 600;
}

.firmness-option {
  display: flex;
  align-items: center;
  gap: 0.45em;
  padding: 0.55em 0.85em;
  border: 1px solid var(--sand-200);
  border-radius: var(--radius);
  cursor: pointer;
}

.firmness-option input[type="radio"] {
  accent-color: var(--moss-700);
}

/* :has() is progressive enhancement, same reasoning already applied to
   the section-cut fallback above — a browser without support just shows
   every option in its plain resting state, still fully legible and fully
   operable via the native radio semantics alone. */
.firmness-option:has(:checked) {
  border-color: var(--moss-700);
  background-color: var(--sand-100);
}

/* calc(60rem * 400 / 720): same scale factor as #education figure (below),
   applied to this diagram's 400-unit viewBox instead of that one's 720 —
   see the scale note on .firmness-layout above. Must stay in sync
   with #education figure's max-width and this SVG's viewBox width. */
.firmness-figure {
  max-width: calc(60rem * 400 / 720);
  margin-inline: auto;
}

.firmness-diagram {
  width: 100%;
  height: auto;
}

.firmness-note {
  margin-top: 1em;
  color: var(--moss-700);
  font-style: italic;
}

.firmness-layers {
  opacity: 0;
}

.firmness-layers.is-active {
  opacity: 1;
}

@media (prefers-reduced-motion: no-preference) {
  .firmness-layers {
    transition: opacity var(--motion-fast);
  }
}

/* ---------- Showroom ---------- */
/* .locations wraps the repeated .location cards so a second (or third)
   showroom can be added later as another sibling .location — the
   showroom count is not yet confirmed — without touching
   this wrapper or its grid rule. auto-fit means one location renders as
   a single card at a sane max width instead of stretching edge to edge;
   two or three lay themselves out in a row (then wrap) automatically. */

.locations {
  display: grid;
  /* min(24rem, 100%) so the track's floor yields to a narrow viewport
     instead of forcing a fixed 24rem minimum that overflows it — plain
     minmax(24rem, 36rem) caused horizontal scroll at 390px, where the
     content column is well under 24rem wide. */
  grid-template-columns: repeat(auto-fit, minmax(min(24rem, 100%), 36rem));
  gap: clamp(1.5rem, 4vw, 2.5rem);
}

/* Card treatment: 1px --sand-200 border, no shadow, the one shared
   --radius. #showroom's
   ground is --sand-100, so a --paper fill gives the card its own edge
   against that ground rather than relying on the border alone.
   Two internal columns (contact info / hours) rather than one long
   stack, so a single location reads as a deliberately laid-out card
   instead of a narrow strip of text stranded in a wide section. */
/* Column 2's floor was originally a guessed
   fixed minmax(9rem, 1fr) — 144px. That guess predates the hours strings
   moving from 24-hour to 12-hour format ("10:00 AM - 4:30 PM"), which is
   wider than "10:00 - 16:30" was. Because the floor was a definite length
   rather than the keyword `auto`, the grid track sizing algorithm doesn't
   propagate the .hours grid's own content-based minimum up through it (a
   definite minmax() minimum opts a track out of that automatic-minimum
   contribution) — so column 2 was free to resolve narrower than the
   nowrap hours text actually needs, and that text overflowed straight
   through it, out of .location, out of the page. Same failure mode
   documented at length above for .firmness-layout's column 1, just not
   yet fixed here. minmax(max-content, 1fr) makes the column exactly as
   wide as the hours content needs (dt + gap + nowrap dd), at every width,
   the same fix already used elsewhere on this page for this exact class
   of bug. That still leaves a width where both columns' true minimums
   (13rem + this column's max-content + the 2rem gap) exceed the
   container — confirmed by sweep to be 485-580px — so the single-column
   breakpoint below moves from 480px to 600px to clear it with margin. */
.location {
  display: grid;
  grid-template-columns: minmax(13rem, 1.4fr) minmax(max-content, 1fr);
  gap: 1.5rem 2rem;
  padding: 2rem 2.25rem;
  background-color: var(--paper);
  border: 1px solid var(--sand-200);
  border-radius: var(--radius);
}

.location-contact > * + * {
  margin-top: 1em;
}

@media (max-width: 600px) {
  .location {
    grid-template-columns: 1fr;
  }
}

/* Shared by #showroom and the footer — a two-column dt/dd grid instead
   of the default dl block stack, so hours read as a compact table
   without needing a real <table> for two columns of short text. */
.hours {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 1em;
  row-gap: 0.35em;
}

.hours dt {
  font-weight: 600;
}

/* Presentation-only fix, not a text change (see .keep-together above for
   the same rule applied to this page): each dd is a short, fixed phrase
   ("10:00 - 16:30", "By appointment") that has enough column width to
   fit on one line at every size this page renders at, but without this
   the browser sometimes breaks it anyway rather than reflowing the
   grid's own column width. */
.hours dd {
  white-space: nowrap;
}

/* ---------- Shot list ---------- */
/* Four framed, captioned slots — not stock or generated imagery. They
   are framed and captioned art direction, never empty gray boxes. The
   frame area carries three deliberate, entirely abstract
   cues that a flat gray box does not: two opposite-corner viewfinder
   brackets, a running shot number, and a fine diagonal hairline texture
   — all built from existing tokens, none of them a depiction of the
   showroom, product, staff, or a customer. The caption below reads like
   a photograph's printed caption, not a broken-image label. */

.shots {
  counter-reset: shot;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(1.5rem, 4vw, 2.5rem);
}

@media (max-width: 640px) {
  .shots {
    grid-template-columns: 1fr;
  }
}

.slot {
  background-color: var(--paper);
  border: 1px solid var(--sand-200);
  border-radius: var(--radius);
  overflow: hidden;
}

.slot__frame {
  position: relative;
  aspect-ratio: 4 / 3;
  background-color: var(--sand-100);
  /* Abstract hairline texture only — no photographic or generated
     content. Every color is an existing token. */
  background-image: repeating-linear-gradient(
    135deg,
    var(--sand-100) 0,
    var(--sand-100) 9px,
    var(--sand-200) 9px,
    var(--sand-200) 10px
  );
  /* Feeds .slot__number below via counter(); incremented here (once per
     frame) rather than on the <ol> or <li>, since this is the element
     that owns the frame's visual identity. */
  counter-increment: shot;
}

.slot__frame::before,
.slot__frame::after {
  content: "";
  position: absolute;
  width: 1.5rem;
  height: 1.5rem;
  border: 2px solid var(--moss-700);
}

/* Two opposite corners only (top-left / bottom-right) — the same
   shorthand a camera viewfinder or crop tool uses to mark a frame
   without drawing its full outline. Reads immediately as "this is
   where a photograph goes," which a plain bordered box does not. */
.slot__frame::before {
  top: 1rem;
  left: 1rem;
  border-right: 0;
  border-bottom: 0;
}

.slot__frame::after {
  bottom: 1rem;
  right: 1rem;
  border-left: 0;
  border-top: 0;
}

/* Shot number, generated from a CSS counter rather than hardcoded per
   <li> so the four slots can't drift out of sequence independently of
   their markup order. Decorative — the parent .slot__frame carries
   aria-hidden, and the <ol> already conveys "item N of 4" to assistive
   tech via native list semantics. */
.slot__number {
  position: absolute;
  top: 1rem;
  right: 1rem;
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.05em;
  color: var(--moss-700);
}

.slot__number::before {
  content: "Shot " counter(shot, decimal-leading-zero);
}

/* Caption reads like a printed photo caption — set off from ordinary
   body copy by italic and the same --moss-700 used for ledes elsewhere
   on the page (10.03:1 on --paper, well past AA). */
.slot figcaption {
  padding: 1.25rem 1.5rem;
  color: var(--moss-700);
  font-style: italic;
}

/* ---------- Footer ---------- */
/* Address, phone, hours, and the two verified social links only — no
   builder credit, no wordmark repeat, no legal boilerplate. */

.footer-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: start;
  gap: 2rem;
}

.footer-info > * + * {
  margin-top: 0.5em;
}

.footer-links {
  display: flex;
  gap: 1.5rem;
}

.footer-links a {
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ---------- Motion ---------- */
/* Transitions only on color/background-color/border-color/opacity,
   150ms ease-out, and only when the user has not asked for reduced
   motion. Nothing else animates on this page. */

@media (prefers-reduced-motion: no-preference) {
  a,
  button,
  .cta {
    transition:
      background-color var(--motion-fast),
      color var(--motion-fast),
      border-color var(--motion-fast);
  }
}
