/* ============================================================
   Deviceland — Design System
   ============================================================ */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--head-h-stuck, 78px) + 26px);
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--on-paper);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.5;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

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

a {
  color: inherit;
  text-decoration: none;
}

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

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

dl,
dd,
dt,
h1,
h2,
h3,
p {
  margin: 0;
}

:root {
  /* Ink is a warm espresso, not black. It is a text and control colour only —
     no section on this page uses it as a ground. A black band is the cheapest
     way to look expensive and the fastest way to look like everyone else. */
  --ink: #241a13;
  --ink-2: #2e2219;
  --ink-3: #3a2c20;
  --ink-4: #4a382a;
  --on-ink: #fff9f5;

  /* Warmed, and the ladder spread out, so that an untouched Apple packshot on
     its mandatory white ground reads as a plate instead of a hole. Full
     derivation and the measured luminance table live on the same tokens in
     assets/css/tokens.css — this block is the homepage's copy and must not
     drift from it. */
  --paper: #f8f2e8;
  --paper-2: #f2ebde;
  --paper-3: #eae1d0;
  /* oklch(99% .004 90) — a step lighter than paper, for the picks ground */
  /* No longer used as a band. A white well on it is a 2.72-point step and so
     invisible; see the mat rule in assets/css/tokens.css. */
  --bone: #fdfcf8;
  --on-paper: #14110e;

  /* House grade for the documentary photography.

     Measured, the two photo sets were living in different worlds:

                     warm   sat   lum   cool px
       studio/bento    39    75    83     2%
       documentary      8    32    49    24%

     A 34-point luminance drop and a halved saturation is what reads as
     "sophisticated, then suddenly not". This closes the gap to warm 21 /
     sat 47 / lum 57 / cool 15% — it does not erase it, because a workbench
     photographed under shop lighting should not pretend to be a seamless
     cove, and pushing it to 83% luminance would only look blown out.

     This is grading context, not merchandise. See the note in the README:
     never grade a photograph where the product's true colour is the thing
     being sold; always grade the room around it. */
  --grade-real: brightness(1.18) contrast(0.93) saturate(0.82) sepia(0.34);
  /* Same family, for photographs that are already high-key. --grade-real lifts
     exposure 18%, which is right for a mid-key bench shot and destructive on a
     bright one. Measured share of pixels clipped to pure white:

                        raw    --grade-real   --grade-real-hi
       owner portrait   4.3%      25.9%            10.8%
       bench technician   0%      18.9%             2.9%
       storefront      13.8%      52.6%            32.2%

     A quarter of a portrait blown to paper-white is not a grade, it is a
     deletion — and faces are the one subject where losing modelling is
     immediately legible as "cheap photo". This variant raises contrast instead
     of exposure, and measures higher local contrast than --grade-real does
     (sd 86 vs 83) while keeping the same warm cast that unifies the set. */
  --grade-real-hi: contrast(1.1) saturate(0.9) sepia(0.22);

  /* Third tier, for the sunlit exterior. The storefront is a bright outdoor
     photograph at lum 173 — 40 points above anything shot indoors — and both
     of the grades above make it worse, not better:

                              lum   blown to pure white
       raw                    173         13.8%
       --grade-real           197         52.6%   <- over half the building
       --grade-real-hi        181         32.2%
       --grade-real-sun       169          7.0%

     Over half a photograph clipped to paper-white is not a warm cast, it is
     lost architecture — the brick, the awning edge and the window frames stop
     existing. The correction a bright exterior needs is negative: hold it
     DOWN, add a little contrast, and carry the same warm cast (warm +26 here
     against +27 for --grade-real, so it still reads as the same family). */
  --grade-real-sun: brightness(0.93) contrast(1.04) saturate(0.9) sepia(0.22);

  /* The one chapter break. Warm sand carries the same structural job a dark
     band was doing — it separates — without borrowing gravity it hasn't earned. */
  --sand: #e7dbc4;
  --sand-2: #dccdb0;
  --on-sand: #2a2018;

  /* Tailwind `bg-surface` in the reference. The category photographs carry a
     warm cream ground of their own (sampled: 251-255, 237-248, 209-227), so
     the tile colour has to sit just above the lightest of them or the scrim
     shows a seam where it lands. */
  --surface: #fdfaf3;
  --surface-80: rgba(253, 250, 243, 0.8);
  /* Was #7d7365, which measured 4.36:1 on --paper and 4.46:1 on --surface —
     under the 4.5:1 AA floor on both. This token carries .tile-price, so the
     failing text was the price: "From $999" was the least legible thing on a
     retail tile. #756b5d is the smallest darkening that clears AA on the
     lighter of the two grounds (4.89:1 paper, 5.01:1 surface) and so keeps the
     warm grey reading the original was tuned for. */
  /* Re-solved against the DARKEST ground it is used on rather than the
     lightest. #756b5d was already failing AA on --paper-3 (4.14) and --sand
     (3.82) — the footer and the whole service chapter — before the paper rungs
     moved. Same hue line, 4.51:1 on --sand, 5.55:1 on --paper. See
     assets/css/tokens.css for the full table. */
  --muted-fg: #696053;

  /* Sampled from the official Deviceland lockup: #c95928 */
  --ember: #c95928;
  --ember-lift: #e4713c;
  --ember-deep: #9c3f18;
  --on-ember: #fff9f5;
  /* Large saturated fields need more light and more chroma than a 40px mark:
     the logo value spread across a whole section reads as rust, not energy. */
  --ember-field: #d9601f;
  --ember-field-2: #b8410f;

  --sans: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Helvetica, Arial, sans-serif;
  --display: "Instrument Serif", "Times New Roman", Georgia, serif;

  /* Named for the band they belong to, not the value they used to hold: these
     are the hairlines on the sand chapter and the footer. */
  --hair-dark: rgba(42, 32, 24, 0.15);
  --hair-dark-2: rgba(42, 32, 24, 0.28);
  --hair-light: rgba(20, 17, 14, 0.11);
  --hair-light-2: rgba(20, 17, 14, 0.22);

  --sh-1: 0 1px 2px rgba(20, 17, 14, 0.05),
    0 8px 20px -12px rgba(20, 17, 14, 0.2);
  --sh-2: 0 2px 4px rgba(20, 17, 14, 0.04),
    0 26px 50px -26px rgba(20, 17, 14, 0.26);
  --sh-3: 0 3px 6px rgba(20, 17, 14, 0.05),
    0 50px 90px -44px rgba(20, 17, 14, 0.42);
  --sh-prod: 0 34px 44px -28px rgba(20, 17, 14, 0.55),
    0 90px 120px -70px rgba(20, 17, 14, 0.4);

  /* max-w-[1480px] px-6 lg:px-10 — 1400px of content at desktop, which is what
     the reference's column arithmetic is built on. At 1400/40 the bento
     columns come out 27px narrow and nothing else lines up either. */
  --shell: 1480px;
  --gut: 24px;

  /* One film-grain source for every saturated ground on the page.
     feTurbulence at baseFrequency 0.7 / 3 octaves, matching the reference
     build's film-grain layer. Held as a token because it is now used on the
     hero's copy panel, the Trade-In tile and the Visit chapter, and three
     copies of a 200-byte data URI is three chances for them to drift.
     Opacity is always set by the consumer, never here: grain reads far
     stronger on a saturated ground than on a light one, so the same texture
     wants ~5% on ember and ~2.5% on sand. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='256'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='256' height='256' filter='url(%23n)'/%3E%3C/svg%3E");

  --ease: cubic-bezier(0.16, 1, 0.32, 1);
  --ease-io: cubic-bezier(0.65, 0, 0.35, 1);

  /* Motion budget. Ding & Kyung (2025): moderate speed beats both fast and
     slow. Nothing on this page animates outside this band. */
  --dur-micro: 150ms;
  --dur-ui: 250ms;
  --dur-reveal: 450ms;
  --dur-slow: 700ms;

  /* Two rows: the bar, and the folio of credentials beneath it. --head-h is
     the SUM, because that is what every `calc(var(--head-h) + N)` rule uses to
     clear the fixed header. See tokens.css for the full note — this block is
     the homepage's copy and must not drift from it. */
  --bar-h: 74px;
  /* 0: the credential row was removed from the masthead (Oct 11). */
  --folio-h: 0px;
  --head-h: calc(var(--bar-h) + var(--folio-h));
  --head-h-stuck: 66px;

  /* Radius: SQUARED, 2px. Full reasoning lives on the same tokens in
     assets/css/tokens.css — including why --r-pill is redefined rather than
     renamed across its 50 call sites.

     THIS COPY IS WHY THE FIRST ATTEMPT DID NOT WORK. deviceland.css loads
     after tokens.css, so these values win on the homepage. Squaring tokens.css
     alone changed nothing visible: .btn resolved var(--r-pill) to the 999px
     defined right here and every button stayed a capsule. The block above
     already warns "this block is the homepage's copy and must not drift from
     it" — it drifted the moment only one copy was edited. Any future change to
     a token must be made in BOTH files. */
  --r-sm: 2px;
  --r-md: 3px;
  --r-lg: 4px;
  --r-xl: 4px;
  --r-pill: 2px;
  --r-tile: 3px;

  /* Elevation: four steps. The same audit found ten distinct box-shadows, nine
     of them used exactly once — ten unrelated "heights" with no logic, which is
     the difference between a design system and accumulated CSS.

     Each step is two shadows: a tight contact shadow for the object's own edge,
     and a wide diffuse one for the distance it sits above the paper. Warm-tinted
     rather than neutral black, because a cream ground reflects warm light. */
  --e1: 0 1px 2px rgba(20, 17, 14, 0.05), 0 1px 1px rgba(20, 17, 14, 0.04);
  --e2: 0 2px 4px rgba(20, 17, 14, 0.05), 0 8px 18px -8px rgba(20, 17, 14, 0.18);
  --e3: 0 3px 6px rgba(20, 17, 14, 0.05), 0 20px 40px -18px rgba(20, 17, 14, 0.3);
  --e4: 0 6px 14px rgba(20, 17, 14, 0.06), 0 44px 80px -36px rgba(20, 17, 14, 0.42);
  /* On the ember ground the same steps need a warmer, deeper tint or they read
     as grey smudges rather than shadow. */
  --e3-warm: 0 3px 6px rgba(60, 20, 0, 0.12), 0 20px 40px -18px rgba(60, 20, 0, 0.4);
  --e4-warm: 0 6px 14px rgba(60, 20, 0, 0.14), 0 44px 80px -36px rgba(60, 20, 0, 0.5);

  /* Ember grounds that carry small text now use the existing --ember-deep
     (#9c3f18), which measures 6.42:1 against white. The bright --ember tops out
     at 3.94:1, meaning NOTHING small could reach the 4.5:1 AA threshold on it
     at any opacity — the ground itself was the failure, not the text colour.
     --ember stays as the accent on light paper, where it has contrast to spare. */
  --ember-deep-2: #7d310f;
}

@media (min-width: 1024px) {
  :root {
    --gut: 40px;
  }
}

::selection {
  background: var(--ember);
  color: var(--on-ember);
}

:focus-visible {
  outline: 2px solid var(--ember-lift);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ------------------------------------------------------------
   Type primitives
   ------------------------------------------------------------ */

/* Instrument Serif is a single-weight display face: no synthetic bolding
   anywhere, and only a hair of negative tracking — it is already tightly fit. */
.display {
  font-family: var(--display);
  font-weight: 400;
  letter-spacing: -0.015em;
  line-height: 0.95;
  text-wrap: balance;
}

/* One rule for the whole page: the italic word is the ember word. Colour is a
   system, not a decoration — it lands in the same grammatical slot every time. */
.display em {
  font-style: italic;
  letter-spacing: -0.004em;
  color: var(--ember);
}

.band-ink .display em {
  color: var(--ember);
}

/* Negative leading is the editorial signal: at 1.0 a headline reads as a web
   page, at 0.86 it reads as a printed spread. Rank is carried by size ratio —
   h-xl : h-lg is 1.8 : 1, so the eye never has to guess what it is reading. */
.h-xl {
  font-size: clamp(3.1rem, 8.4vw, 7.5rem);
  line-height: 0.86;
}
/* text-[clamp(2.5rem,6vw,5.5rem)] leading-[0.95] tracking-[-0.025em] */
.h-lg {
  font-size: clamp(2.5rem, 6vw, 5.5rem);
  line-height: 0.95;
  letter-spacing: -0.025em;
}
.h-md {
  font-size: clamp(2.05rem, 3.78vw, 3.4rem);
  line-height: 0.95;
}
.h-sm {
  font-size: clamp(1.5rem, 2.1vw, 2.05rem);
  line-height: 1;
}
/* text-[clamp(2.4rem,4.6vw,3.8rem)] leading-[0.96] — the picks heading */
.h-picks {
  font-size: clamp(2.4rem, 4.6vw, 3.8rem);
  line-height: 0.96;
}

.eyebrow {
  font-size: 11px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.28em;
  line-height: 1;
}

.lede {
  font-size: 15px;
  line-height: 1.62;
  text-wrap: pretty;
}

.num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* Both "dimmer" tiers were below AA against the ground they actually sit on,
   and both carry .eyebrow — 11px uppercase with tracking, i.e. the hardest
   text on the page to read even at full contrast.

   Composited and measured against their real grounds:

                        was          now
     .on-ink-dimmer     3.82:1  ->   4.78:1   on --sand  #e7dbc4
     .on-ember-dimmer   3.72:1  ->   4.56:1   on --ember-deep #9c3f18

   --ember-deep leaves so little headroom that 0.78 nearly collapses the
   dimmer tier into the dim one. Rather than ship two tiers that look
   identical, the dim tier is lifted to 0.92 so the rank stays visible and
   both ends clear the threshold. */
.on-ink-dim {
  color: rgba(42, 32, 24, 0.74);
}
.on-ink-dimmer {
  color: rgba(42, 32, 24, 0.68);
}
.on-paper-dim {
  color: rgba(22, 19, 15, 0.66);
}
.on-ember-dim {
  color: rgba(255, 249, 245, 0.92);
}
.on-ember-dimmer {
  color: rgba(255, 249, 245, 0.78);
}

.ember-text {
  color: var(--ember);
}

.band-ember .display em,
.band-ember .ember-text {
  color: var(--on-ember);
  opacity: 0.82;
}

/* Ember on the value word only — one tinted word per headline, never two */
.tint {
  color: var(--ember);
}

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

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gut);
}

/* Anchor offset is owned by the scrollport, not by each target. One rule then
   covers sections, tiles, tables and widgets alike — including the ones added
   later, which is how anchors quietly rot behind a fixed header. */
section {
  position: relative;
}

/* Kept under the old name so every descendant rule stays where it was; only
   the ground changed. Sand does the separating that black used to do. */
.band-ink {
  background: var(--sand);
  color: var(--on-sand);
}
.band-ink-2 {
  background: var(--sand-2);
  color: var(--on-sand);
}
/* The Visit chapter gets the same depth stack as the hero's copy panel.
   The vignette rides on the section's own box-shadow rather than a
   pseudo-element, because box decoration paints below all content and so
   cannot cover the grid on top of it. The grain has to be a pseudo-element to
   carry its own opacity, and it is ::before rather than ::after: both it and
   .visit-grid are positioned at z-index auto, so paint order follows the DOM
   and ::after would sit on top of the copy. */
.band-ember {
  position: relative;
  box-shadow: inset 0 0 170px rgba(38, 12, 0, 0.4);
  background: linear-gradient(
    152deg,
    var(--ember-deep) 0%,
    var(--ember-deep) 52%,
    var(--ember-deep-2) 100%
  );
  color: var(--on-ember);
}

.band-ember::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.05;
  background-image: var(--grain);
}

/* Sand is a light ground at 71.6% luminance, and grain at ember strength on
   it reads as dirt rather than as film. 2.5% is the most it takes, with a
   correspondingly gentler vignette. */
.band-ink {
  position: relative;
  box-shadow: inset 0 0 150px rgba(60, 40, 18, 0.14);
}

.band-ink::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.025;
  background-image: var(--grain);
}
.band-paper {
  background: var(--paper);
  color: var(--on-paper);
}
.band-paper-2 {
  background: var(--paper-2);
  color: var(--on-paper);
}
.band-bone {
  background: var(--bone);
  color: var(--on-paper);
}

/* py-20 lg:py-32 */
.pad-y {
  padding-block: 80px;
}

@media (min-width: 1024px) {
  .pad-y {
    padding-block: 128px;
  }
}

/* Where two chapters share a ground colour the gap between them has nothing to
   read as, and 236px of flat paper looks unfinished rather than generous. A
   hairline turns the same space into a chapter rule. */
.band-paper + .band-paper,
.band-paper-2 + .band-paper-2 {
  border-top: 1px solid var(--hair-light);
}

.band-ink + .band-ink {
  border-top: 1px solid var(--hair-dark);
}

/* fixed, not absolute.
   As an absolutely positioned element its containing block was not the
   viewport, so the offsets did not mean what they read as: measured, computed
   top was -60px while the element's actual rect.top was 36 — a 96px
   discrepancy, which put the focused link across the Deviceland lockup no
   matter what value was set here. Two attempts at tuning `top` therefore did
   nothing, because the number was never the problem.
   position: fixed makes the viewport the containing block unconditionally, so
   top means the distance from the top of the screen and the focused link
   lands below the header where it was always meant to. A skip link is
   viewport furniture anyway; fixed is the right position for it. */
.skip {
  position: fixed;
  left: 16px;
  top: -60px;
  z-index: 999;
  background: var(--on-ink);
  color: var(--ink);
  padding: 10px 16px;
  border-radius: var(--r-pill);
  font-size: 13px;
  font-weight: 500;
  transition: top 0.2s var(--ease);
}

/* Below the masthead, not on top of it. At 16px the focused link landed
   inside the 96px fixed header, directly across the Deviceland lockup — the
   one element on the page that should never be covered. This is the base rule
   rather than a per-layout override because the header is fixed at the top of
   every layout, so 16px was wrong on all of them. */
.skip:focus {
  top: calc(var(--head-h) + 12px);
}

/* ------------------------------------------------------------
   Grain — perceived-craft layer
   ------------------------------------------------------------ */

/* Was z-index 900 with mix-blend-mode: multiply — i.e. a noise texture
   multiplying over EVERY element on the page, product photography included.
   Multiply darkens and dirties, so this was quietly grading the merchandise,
   which is the exact thing the grading rules in the README forbid. Apple never
   lays texture over product.

   It now sits behind the content (z-index 0, .page is z-index 1), so it still
   gives the paper its tooth and never touches a product. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

.page {
  position: relative;
  z-index: 1;
}

/* ------------------------------------------------------------
   Buttons
   ------------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  height: 50px;
  padding-inline: 26px;
  border-radius: var(--r-pill);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -0.005em;
  white-space: nowrap;
  transition: transform 0.35s var(--ease), background 0.35s var(--ease),
    border-color 0.35s var(--ease), box-shadow 0.35s var(--ease),
    color 0.35s var(--ease);
}

.btn svg {
  transition: transform 0.4s var(--ease);
}

.btn:hover svg {
  transform: translateX(3px);
}

.btn:active {
  transform: scale(0.985);
}

.btn-ember {
  background: linear-gradient(168deg, var(--ember-lift), var(--ember) 62%, var(--ember-deep));
  color: var(--on-ember);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.22) inset,
    0 10px 30px -12px rgba(201, 89, 40, 0.6);
}

.btn-ember:hover {
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.28) inset,
    0 16px 40px -12px rgba(201, 89, 40, 0.72);
  transform: translateY(-1px);
}

.btn-outline {
  border: 1px solid var(--hair-light-2);
  color: var(--on-paper);
}

.btn-outline:hover {
  background: rgba(20, 17, 14, 0.05);
  border-color: rgba(20, 17, 14, 0.38);
}

.btn-ink {
  background: var(--ink);
  color: var(--on-ink);
  box-shadow: var(--sh-1);
}

.btn-ink:hover {
  transform: translateY(-1px);
  box-shadow: var(--sh-2);
}

/* The sand band is light, so the primary control on it is ink, same as
   everywhere else. One primary button, one appearance, whole page. */
.btn-light {
  background: var(--ink);
  color: var(--on-ink);
  box-shadow: var(--sh-1);
}

.btn-light:hover {
  transform: translateY(-1px);
  box-shadow: var(--sh-2);
}

.btn-ghost-ink {
  border: 1px solid var(--hair-dark-2);
  color: var(--on-sand);
}

.btn-ghost-ink:hover {
  background: rgba(42, 32, 24, 0.06);
  border-color: rgba(42, 32, 24, 0.4);
}

.btn-on-ember {
  background: var(--on-ember);
  color: var(--ember-field-2);
  box-shadow: var(--e3-warm);
}

.btn-on-ember:hover {
  transform: translateY(-1px);
  box-shadow: var(--e4-warm);
}

.btn-ghost-ember {
  border: 1px solid rgba(255, 249, 245, 0.42);
  color: var(--on-ember);
}

.btn-ghost-ember:hover {
  background: rgba(255, 255, 255, 0.14);
  border-color: rgba(255, 249, 245, 0.75);
}

.link-quiet {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  font-weight: 500;
  padding-bottom: 3px;
  border-bottom: 1px solid transparent;
  transition: border-color 0.3s var(--ease), color 0.3s var(--ease);
}

.link-quiet svg {
  transition: transform 0.4s var(--ease);
}

.link-quiet:hover {
  border-bottom-color: currentColor;
}

.link-quiet:hover svg {
  transform: translate(2px, -2px);
}

/* ------------------------------------------------------------
   Header
   ------------------------------------------------------------ */

/* At rest the masthead has no surface of its own: it sits on the hero's paper
   so the opening view reads as one continuous field. It earns a surface only
   once it is floating over content. */
.masthead {
  position: fixed;
  inset-inline: 0;
  /* Cleared for WordPress's admin bar; see the note in chrome.css. The value
     is published by deviceland.js because the bar is fixed above 600px,
     absolute below it, and a different height on each. */
  top: var(--dl-admin-offset, 0px);
  z-index: 800;
  background: transparent;
}

.masthead-inner {
  position: relative;
  display: flex;
  align-items: center;
  gap: 32px;
  height: var(--bar-h);
  transition: height var(--dur-reveal) var(--ease);
}

.masthead.is-stuck .masthead-inner {
  height: var(--head-h-stuck);
}

/* The condensed surface is its own layer. Because it is a sibling of the links
   rather than their background, it can scale and fade without ever nudging
   them — the single detail that separates a considered island from a jump. */
/* Scroll-only again, and turning it on at rest was a mistake.
   The pill has to contain the full lockup — monogram, DEVICELAND and the
   "DIGITAL SALES, SERVICE AND SMILES." tagline under it — which forces the
   bar to 96px, so at rest it read as a tall slab rather than a floating
   header. The condensed state is 78px with the wordmark alone and is the
   cleaner of the two, which is the opposite of what I assumed when I made
   the at-rest state visible.
   It is also not what the reference does. Humanity's header is
   `fixed top-0` with no bar at all at rest — just a
   `from-black/40 via-black/20 to-transparent` scrim — and becomes a
   FULL-WIDTH frosted bar on scroll, never a pill. So: transparent at rest,
   island on scroll. */
/* The floating pill is retired. This is a FULL-BLEED flush bar now.
   
   Two reasons, and the first is the house geometry: --r-pill is 2px, so an
   inset capsule renders as a floating 2px rectangle and the whole effect of
   an "island" depends on it reading as a capsule. Squaring the system removes
   the thing that made the shape work.
   
   The second is the folio. The credential row has to sit FLUSH beneath the
   bar as a full-width rule; an inset pill with 8px of transparent margin on
   every side cannot carry a flush rule under it, and the gaps let scrolled
   content show through between the two.
   
   The note this replaces observed that the reference itself does exactly
   this — "Humanity's header is fixed top-0 with no bar at all at rest ... and
   becomes a FULL-WIDTH frosted bar on scroll, never a pill." That was right,
   and the pill was the part I had added.
   
   Stickiness is kept deliberately. Dropping it would cost persistent nav and
   bag access on long shop pages, which on an e-commerce site is a real
   conversion cost rather than a styling preference. */
.island {
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: 0;
  opacity: 0;
  background: rgba(248, 242, 232, 0.92);
  border-bottom: 1px solid rgba(20, 17, 14, 0.1);
  backdrop-filter: blur(22px) saturate(1.6);
  -webkit-backdrop-filter: blur(22px) saturate(1.6);
  transition: opacity var(--dur-reveal) var(--ease);
}

/* No 100vw hack needed: .masthead-inner.shell drops its max-width further
   down, so inset:0 is already full width. The hack was also fragile — a later
   `@media (min-width:1024px) { .island { inset: 0 } }` reset `left` while
   leaving margin-left:-50vw applied, which put half the surface off the left
   edge of the screen on the product page. */
.island {
  left: 0;
  right: 0;
  width: auto;
  margin-left: 0;
}

.masthead.is-stuck .island {
  opacity: 1;
}

/* ------------------------------------------------------------
   The folio — the credential row under the bar
   ------------------------------------------------------------
   This is the signature device of the design and the highest-converting
   element on the page: it puts legitimacy, longevity and the live open state
   inside the buyer's FIRST horizontal scan, before any scrolling.

   Open-ended cells: a bottom rule and vertical dividers, no top border and no
   outer edges. Closing them into boxes was tried and reverted — open reads as
   a folio line in a catalogue, closed reads as a table.

   It collapses to zero on scroll. The credentials have done their job by the
   time someone is reading, and a permanent 40px band of small caps over the
   content would be chrome for its own sake. Because --head-h is bar + folio
   while --head-h-stuck is the bar alone, everything that clears the header
   follows this automatically. */
.folio {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: stretch;
  /* The credentials sit on the right (owner, Oct 11). */
  justify-content: flex-end;
  height: var(--folio-h);
  border-bottom: 1px solid var(--hair-light);
  /* Transparent, so the folio shares the masthead's ground. It was --surface
     (#fdfaf3) against the masthead's --paper (#f8f2e8): a two-point step that
     read as a seam splitting the header into two unrelated bars. */
  background: transparent;
  overflow: hidden;
  transition: height var(--dur-reveal) var(--ease),
    opacity var(--dur-reveal) var(--ease);
}

.folio-cell {
  display: flex;
  align-items: center;
  gap: 9px;
  padding-inline: clamp(14px, 1.8vw, 26px);
  white-space: nowrap;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--ink-4);
}

.folio-cell + .folio-cell {
  border-left: 1px solid var(--hair-light);
}

/* The first and last cells take the page's own gutter, so the folio's text
   starts on the SAME vertical as the wordmark above it and the hero copy
   below it. Measured before this: the logo sat at 42px, the hero copy at 45px
   and the folio's first cell at 27px — three different left edges stacked on
   top of each other in the same 110px of page. */
.folio-cell:first-child {
  padding-left: clamp(26px, 3vw, 54px);
}

.folio-cell:last-child {
  padding-right: clamp(26px, 3vw, 54px);
}

/* pushed to the right edge, and it takes its divider with it */
.folio-cell.is-right {
  margin-left: auto;
  border-left: 1px solid var(--hair-light);
}

/* The only live element in the header. --ember when open; deviceland.js adds
   .is-shut when the shop is closed and the dot goes grey, because a lit pip
   at 11pm is worse than no pip. */
.folio-pip {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--ember);
  flex: 0 0 auto;
}

.folio-cell .is-shut .folio-pip,
.folio-cell.is-shut .folio-pip {
  background: var(--muted-fg);
}

.masthead.is-stuck .folio {
  height: 0;
  opacity: 0;
  border-bottom-color: transparent;
}

/* Below 1024 the folio keeps only the two cells that earn their width: the
   authorization and the live hours. Five cells of tracked caps cannot fit a
   phone without either wrapping to two rows or scrolling sideways, and both
   look broken. */
@media (max-width: 1023px) {
  .folio-cell.is-optional {
    display: none;
  }
}

@media (max-width: 767px) {
  .folio {
    height: 34px;
  }

  .folio-cell {
    padding-inline: 14px;
    font-size: 9px;
    letter-spacing: 0.2em;
  }

  /* Only the authorization and the live hours survive here. Three nowrap
     cells measured wider than a 390px viewport, and because they cannot wrap
     the folio set the document's scrollWidth — which pushed the lede and the
     masthead icons off the side of the screen. The dateline is the one to
     drop: the band below carries the full street address. */
  .folio-cell.is-optional-sm {
    display: none;
  }

  /* Belt and braces: if a translation ever makes these strings longer, the
     folio truncates itself rather than widening the document. */
  .folio {
    max-width: 100%;
  }

  .folio-cell {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* On phones the condensed surface goes full-bleed instead of floating.

   As an inset pill (8px top, 12px sides) it left a gap on every edge, and the
   masthead itself is transparent — so scrolled hero content showed THROUGH those
   gaps. In practice that meant the open-state pill appearing above the island as
   a sliced band, and the h1 cut in half by the island's lower edge. It reads as a
   rendering fault rather than a floating header.

   There is no room for a floating pill on a 390px screen anyway: the inset costs
   24px of the 390 available, and the effect only reads at a width where the pill
   is visibly narrower than the page. */
/* ---- Phones get a plain bar, and that is the entire mechanism. -------------

   Three separate attempts failed to stop the lockup appearing sliced on a real
   iPhone, and none of them reproduced in headless Chromium at the same viewport
   across a 15px scroll sweep from 0 to 420px on four viewport sizes. When
   geometry measures clean and the device still shows a defect, the defect is not
   in the geometry — so guessing at it again would be the fourth guess.

   What the desktop header actually does is stack four things iOS Safari is known
   to handle badly, all at once, and toggles them on the same frame the URL bar
   collapses:

     1  a child at z-index:-1 inside position:fixed
     2  a transform animating scale(.985) -> scale(1) on that child
     3  backdrop-filter: blur(22px) saturate(1.6) on it
     4  an animated HEIGHT on the flex row beside it

   And on short viewports there was a genuine, measurable bug feeding it:
   --head-h was overridden to 76px while --head-h-stuck stayed at its default
   78px, so the header GREW by 2px on scroll rather than condensing, with a
   height transition running and the logo swapping from a 48px lockup to a 17px
   wordmark inside it. A header that animates taller while its contents shrink is
   exactly what "cuts off when I start scrolling" describes.

   So phones no longer get any of it. No island, no transform, no backdrop
   filter, no negative z-index, no height animation. A constant-height bar that
   turns opaque. It loses the frosted-glass condense — which was never legible at
   415px — and it cannot fail. */
/* Phones keep the desktop island and the desktop logo condense. The ONE thing
   they do differently is hold the bar at a constant height.

   That distinction matters, because the previous pass deleted the island, the
   blur and the logo swap together — and the actual defect was none of those. It
   was that --head-h had been overridden to 76px for short viewports while
   --head-h-stuck stayed at its 78px default, so the header animated 2px TALLER
   on scroll, with `transition: height` running, while the logo shrank from a
   48px lockup to a 17px wordmark inside it. Fixing the token pair removes the
   cause, so the effect can stay. */
@media (max-width: 767px) {
  /* Height is fixed across the condense. There is no height transition to catch
     mid-frame, and stuck and unstuck can no longer disagree about how tall the
     bar is — which is what produced a half-drawn lockup. Only the island's
     opacity and the logo asset change now. */
  .masthead-inner,
  .masthead.is-stuck .masthead-inner {
    height: var(--bar-h);
    transition: none;
  }

  /* The pill is the content's own box, not a separate inset.
     ------------------------------------------------------------
     Every previous attempt tuned two independent numbers — the island's inset
     from .masthead-inner, and the content's inset via .shell's gutter — and
     the clearance between them was whatever those two happened to leave. That
     is why it kept coming out wrong: 12px, then 10px, then 4px, each time
     re-derived by hand against a control count and asset widths that had
     already changed once.

     Now .masthead-inner IS the pill's outer bound: it is pulled in 4px from
     the viewport and the island simply fills it horizontally. Clearance on
     both sides then equals .shell's own padding — one number, symmetric by
     construction, and it tracks --gut automatically instead of being a
     coincidence between two rules.

     width: auto is required. .shell sets width: 100%, which with a margin
     would compute 100% of the parent and overflow by 8px rather than inset. */
  .masthead-inner {
    width: auto;
    margin-inline: 0;
  }

  /* inset:0, not 6px. The inset made the bar read as a floating island, but
     the masthead is fixed, so those 6px were a slit across the whole screen
     with the page scrolling behind it. Flush on a phone. */
  .island {
    inset: 0;
  }

  /* The masthead itself stays transparent so the island reads as floating rather
     than as a full-width bar with a pill drawn on it. */
  .masthead.is-stuck {
    background: transparent;
    box-shadow: none;
  }

  /* The wordmark is now permanent on phones, not only when condensed.

     The lockup does not fit beside the controls at ANY phone width, and the
     old arithmetic missed it because it was done when the masthead had two
     controls. The comment below this block still reads "the call link plus
     menu button are 64px" — that was true before Account and Bag were added
     back for the real cart and account area. With four controls the actions
     column is ~157px, and nothing between 360px and 767px was re-measured.

     Measured at 375px, which is what the island was visibly overflowing:
       content box            375 - 48 (gutters)      = 327px
       full lockup            452x104 scaled to 48h   = 209px
       actions                37 + 3x38 + 3x2 gap     = 157px
       row gap                                          32px
       total                                           398px  -> 71px over

     Swapping to the wordmark and tightening the gap:
       150 + 16 + 157 = 323px, inside 327px.

     This also makes the phone header identical at rest and when stuck, which
     is consistent with the fixed height already set above. */
  .masthead-inner {
    gap: 16px;
  }

  .logo .logo-full,
  .masthead.is-stuck .logo .logo-full {
    display: none;
  }

  .logo .logo-word,
  .masthead.is-stuck .logo .logo-word {
    display: block;
  }

  /* Structural containment, because arithmetic alone kept being wrong.

     Two passes of measuring widths and tightening gaps both left the controls
     spilling past the island, because a measured sum only holds for the exact
     asset widths, font metrics and control count it was measured against — and
     the control count has already changed once. A flex row with a
     non-shrinkable logo will always overflow eventually.

     So the priority is declared instead of computed. The actions never shrink
     and never wrap; the logo yields first and clips its own overflow. If space
     runs out the wordmark loses a few pixels off its right edge, which is
     recoverable and nearly invisible, rather than the bag and menu buttons
     being pushed outside the pill, which is neither. */
  .masthead-inner {
    flex-wrap: nowrap;
  }

  /* padding-right is set in the consolidated phone block near the end of this
     file, because .masthead.is-stuck .masthead-actions is declared after this
     point and would otherwise win on source order. */
  .masthead-actions {
    flex: 0 0 auto;
  }

  .logo {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
  }

  .logo .logo-img {
    max-width: 100%;
  }
}

/* The control-shrink media queries live AFTER the .icon-btn definition, near
   the bottom of this file. They were here originally and never took effect:
   .icon-btn's base rule is declared later in the source, and at equal
   specificity the later declaration wins whether or not the earlier one is
   inside a media query. Verified in the generated chrome.css, where the 36px
   and 34px rules both preceded the 38px base. */

/* The narrowest phones. The wordmark swap now happens at 767px above, so what
   is left here is the call button.

   iPhone SE 1 is 320px: content is 272px, and even with the wordmark and 34px
   controls the row needs 150 + 12 + 145 = 307px. The call button is the right
   thing to drop rather than shrinking type or the mark, because the call bar
   at the bottom of the viewport already carries "Call the shop" and never
   scrolls away — which is what the note beside .masthead-call has said all
   along. Without it: 150 + 12 + 106 = 268px inside 272px.

   Note for future edits: this is where adding a fifth control will break
   first. The header is width-bound on phones, not height-bound. */
/* The call button is hidden across all phone widths in the consolidated block
   near the end of this file, so only the gap tightens here. */
@media (max-width: 359px) {
  .masthead-inner {
    gap: 12px;
  }
}

/* Status bar clearance. The meta tag does not set viewport-fit=cover, so iOS
   should inset the safe area on its own — but a fixed bar at top:0 being drawn
   under the status bar is the most common cause of exactly this symptom, and the
   fallback is 0px, so on every device where it is unnecessary it costs nothing. */
.masthead {
  padding-top: env(safe-area-inset-top, 0px);
}

/* Logo and actions slide inboard so they clear the pill's radius. */
.logo,
.masthead-actions {
  transition: padding var(--dur-reveal) var(--ease);
}

/* The actions still slide inboard to clear the pill radius. The logo does not:
   it becomes a fixed crop window, and padding on that window would eat into
   the wordmark rather than move it. */
.masthead.is-stuck .logo {
  margin-left: 16px;
}

.masthead.is-stuck .masthead-actions {
  padding-right: 12px;
}

/* Logo lockup */
.logo {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  color: var(--on-paper);
}

.logo svg {
  height: 52px;
  width: auto;
  transition: height var(--dur-reveal) var(--ease);
}

.masthead.is-stuck .logo svg {
  height: 36px;
}

.logo .logo-mark {
  transition: transform 0.6s var(--ease);
  transform-origin: 26px 30px;
}

.logo:hover .logo-mark {
  transform: rotate(-6deg) scale(1.06);
}

/* One lockup, one asset, height animated. The reference swaps in a second PNG
   that was cropped without measuring its ink box, which amputates the bottom of
   the D/L monogram and leaves an "n" in the condensed state. Never crop a mark
   to fit a bar; scale the mark and let the bar follow it. */
/* Two states, two crops, both derived from the file's measured ink boxes
   (1012 x 248 source):

     D mark      x 0-149
     wordmark    x 225-1008,  y  62-154   (784 x 93)
     slogan      x 225-1007,  y 194-238

   At rest the whole lockup shows at 52px. That is deliberately larger than
   the old 40px, because at 40px the slogan renders 7.3px tall and is a grey
   smudge — printing a tagline nobody can read is worse than not printing it.
   At 52px it comes out 9.4px and resolves.

   Scrolled, the lockup does not shrink; it changes crop. The D mark and the
   slogan leave and the wordmark alone is shown at 18px, which is 60% larger
   than the wordmark was inside the 30px full lockup while occupying less
   height in the bar. Shrinking a three-part lockup to fit a condensed bar
   makes all three parts illegible at once. Dropping two of them makes the
   third readable. */
/* Two assets rather than one image inside a crop window.

   The old approach positioned a single file inside a fixed overflow box using
   offsets hand-computed from a 1012x248 source. Swap the artwork and the maths
   silently breaks — which is exactly what happened when the authentic lockup
   went in: the window sliced the D off and the masthead read "EVICELAND".

   Both crops are now cut from the real file's MEASURED ink boxes by
   logosplit.mjs (lockup y 17-248, wordmark y 74-162) and toggled by display.
   Nothing to recompute, nothing to drift.

   The Apple Premier signature is deliberately cropped out of both. Apple's
   Channel Affiliate guidelines allow one Apple signature per surface and
   require it to sit secondary to the reseller's own identity; it already
   appears in the credentials card and the footer. */
.logo-img {
  display: block;
  width: auto;
  max-width: none;
  transition: height var(--dur-reveal) var(--ease);
}

/* The wordmark is the masthead logo now, at rest as well as scrolled, and the
   three-part lockup is retired from the header entirely.
   
   It was the full lockup — monogram, DEVICELAND, and "DIGITAL SALES, SERVICE
   AND SMILES." stacked under it — at 52px, which is what forced the bar to
   96px and made the header read as a slab. Two rounds of complaint about
   header height were both really about this asset: the tagline is a third
   line of type in a bar that only needs one.
   
   Nothing is lost. The full lockup still appears at 48px in the footer, which
   is where a three-part identity has room to be read. Phones already forced
   the wordmark for width reasons (see the max-width:767px block above), so
   this makes every breakpoint agree rather than introducing a new state. */
.logo-full {
  display: none;
  height: 48px;
}

.logo-word {
  display: block;
  height: 20px;
}

@media (min-width: 1024px) {
  .logo-word {
    height: 22px;
  }
}

/* Scrolled: same asset, a shade smaller. The swap used to happen here; now
   only the size changes, so there is no asset switch mid-transition to catch
   half-drawn. */
.masthead.is-stuck .logo-full {
  display: none;
}

.masthead.is-stuck .logo-word {
  display: block;
  height: 18px;
}

.logo-invert {
  filter: none;
}

/* Apple channel signature.
   Apple's Identity Guidelines for Channel Affiliates require the signature to be
   rendered all-black or all-white with no tint, kept in a secondary position,
   and used once per surface. It is therefore absent from the masthead and
   appears only in the credentials card and the footer, at full density.

   The supplied file is 260x230 with the mark occupying x 6-259, y 110-219.
   The crop window is derived from that ink box, not guessed: at a 62px render
   width the scale is 62/260 = 0.2385, so the ink measures 60.3 x 26.0 and the
   window must be at least that wide or the final letterform is sliced. */
/* The signature is now a pre-trimmed asset, so there is no crop window and no
   offsets to get wrong.

   It used to be a 61x26 overflow box with the image pushed to top:-26.2px,
   left:-1.4px — numbers derived from a documented ink box of "x 6-259, y
   110-219". The file actually measures x 3-257, y 112-217, so at a 62px render
   the ink landed at L -0.68px and the Apple glyph was sliced off its left edge
   in every section that showed it. Trimming the file to its real ink box
   (257x108, aspect 2.380) removes the entire mechanism. */
/* Width and padding are both derived from Apple's two stated signature limits,
   not chosen. Apple Identity Guidelines for Channel Affiliates (July 2019),
   Minimum Clear Space and Minimum Size, p.10:

     "For all Apple channel signatures, the minimum size is 8 mm in print and
      35 pixels onscreen, measuring the height of the Apple logo."
     "The minimum clear space around the signature is equal to one-half the
      height of the Apple logo... Do not allow photos, typography, or other
      graphic elements to enter the minimum clear space area."

   The constraint is on the APPLE LOGO, not on the file. Measured on the ink of
   apple-premier-trim.png, the glyph occupies y 1-106 of 108 — 98.15% of the
   file height — so:

     rendered logo height = width x (108/257) x 0.9815

   which at the previous width of 62px produced a 26.0px logo: the signature
   was shipping 26% under Apple's floor in the footer, the Receipts block and
   the PDP. Solving for 35px gives a minimum width of 84.85px, and the
   guidelines' own web figure — "a two-line Apple channel signature at the
   recommended size of 35 pixels high on website or email headers" — is the
   size to hit rather than exceed, so 86px it is (logo 35.5px).

   Clear space is half of that: 17.75px, taken to 18. box-sizing is declared
   rather than inherited because this rule is duplicated into chrome.css,
   which is generated without the `*` reset that sets border-box here — with
   padding load-bearing, the two sheets would otherwise disagree by 36px. */
.badge-apple {
  position: relative;
  display: block;
  box-sizing: content-box;
  width: 86px;
  padding: 18px;
  flex-shrink: 0;
}

.badge-apple img {
  display: block;
  width: 100%;
  height: auto;
  filter: brightness(0);
}

.badge-apple.is-reversed img {
  filter: brightness(0) invert(1);
}

/* .cert-badge is gone with the signature it positioned. Two Apple
   authorizations were being displayed inside a block that promotes the M5 Air
   by name and price, and the channel guidelines separate those two contexts
   explicitly (p.13): multiple authorizations belong on "your company's web
   page that describes your business capabilities... an About Us or Overview
   page", and "Do not display multiple authorizations on product promotions."

   The Receipts credential keeps both authorizations as text, which is the
   treatment the guidelines themselves prescribe wherever a signature does not
   fit the context — "refer to your Apple channel authorization in text". The
   one signature this page is allowed now sits in the footer, beside the
   Deviceland lockup, which is the only place on the page where a store
   identity is present for it to be secondary to. */

.foot .logo {
  color: var(--on-sand);
}

.nav {
  display: none;
  flex: 1;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

@media (min-width: 1200px) {
  .nav {
    display: flex;
  }
}

.nav a {
  position: relative;
  padding: 8px 12px;
  font-size: 13px;
  font-weight: 450;
  color: rgba(20, 17, 14, 0.64);
  border-radius: var(--r-pill);
  transition: color 0.3s var(--ease);
}

/* The origin flips between states, so the rule wipes on from the left and
   retracts to the right rather than pulsing from one anchor. */
.nav a::after {
  content: "";
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 2px;
  height: 1px;
  background: var(--ember);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--dur-ui) var(--ease);
}

.nav a:hover {
  color: var(--on-paper);
}

.nav a.is-active {
  color: var(--on-paper);
}

.nav a:hover::after,
.nav a.is-active::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* ------------------------------------------------------------
   Persistent mobile action bar
   ------------------------------------------------------------ */

.callbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 880;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--hair-light);
  border-top: 1px solid var(--hair-light);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  backdrop-filter: saturate(160%) blur(14px);
  -webkit-backdrop-filter: saturate(160%) blur(14px);
  box-shadow: 0 -12px 30px -18px rgba(20, 17, 14, 0.35);

  /* This bar was setting a 397px MINIMUM WIDTH on the whole document.

     Grid items default to `min-width: auto`, which means they refuse to shrink
     below their min-content size. The two buttons here contain "Call the shop /
     Open until 6pm" and "20790 Hawthorne / Free lot · directions", so `1fr 1fr`
     was resolving to `198px 198px` — 397px including the gap — no matter how
     narrow the device was.

     The visible symptom was a vertical strip of cream down the right-hand edge,
     because the layout viewport was pinned at 397px while in-flow content sized
     to the real viewport. Its width was exactly the shortfall, which is why it
     was obvious on an SE and nearly invisible on a 12 Pro:

       397 - 375 = 21px   plainly visible
       397 - 390 =  6px   "a tiny vertical cream bar"

     minmax(0, 1fr) lets the tracks go below min-content, so the bar fits any
     width and the document floor disappears. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);

  /* Hidden at rest, revealed once the page moves.

     Two reasons, and the first is a platform fact rather than a preference. iOS
     Safari overlays its bottom toolbar on top of fixed-bottom content while the
     URL bar is expanded, which is the state every page loads in — so the bar was
     sliced through its labels before the visitor did anything, and
     env(safe-area-inset-bottom) does not help because this is a toolbar, not a
     home-indicator inset. The toolbar collapses on first scroll. Appearing then
     is the only way to be full height on arrival.

     Second, it is redundant in the first viewport: the hero already shows
     (310) 357-4400 as a button. The bar earns its place the moment that button
     scrolls away, which is precisely when it now appears. */
  transform: translateY(calc(100% + 14px));
  transition: transform 420ms var(--ease);
  will-change: transform;
}

.callbar.is-in {
  transform: none;
}

/* Nothing to reveal for someone who has asked not to be animated: show it
   immediately and let the toolbar overlap resolve on its own. */
@media (prefers-reduced-motion: reduce) {
  .callbar {
    transition: none;
  }
}

/* Desktop has the address in the masthead, a call button in the masthead, and
   a visible hero CTA. The bar would be redundant chrome. */
@media (min-width: 1024px) {
  .callbar {
    display: none;
  }
}

/* 56px, comfortably past both the 24px WCAG 2.5.8 floor and the 44px Apple HIG
   recommendation, and past Material's 48dp. */
.callbar-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  min-height: 56px;
  padding: 8px 12px;
  background: rgba(248, 242, 232, 0.94);
  color: var(--on-paper);
  text-align: left;
  /* The grid track can now be narrower than this button's content, so the button
     has to be willing to be narrower too — otherwise it just overflows its own
     track and the 397px floor comes back one level down. */
  min-width: 0;
}

.callbar-call {
  background: var(--ember-deep);
  color: var(--on-ember);
}

.callbar-btn:active {
  filter: brightness(0.94);
}

.callbar-btn svg {
  flex: 0 0 auto;
}

.callbar-txt {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
  min-width: 0;
}

/* The bold line is the label that matters — "Call the shop", "20790 Hawthorne" —
   so it truncates rather than wraps to two lines and pushes the bar taller. */
.callbar-txt b {
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: 0.005em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.callbar-txt i {
  font-style: normal;
  font-size: 11px;
  letter-spacing: 0.04em;
  opacity: 0.78;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The bar covers the last of the page, so give the footer room to clear it. */
@media (max-width: 1023px) {
  .foot {
    padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
  }
}

/* Sits between the nav and the actions. Shown from 1024px, where there is room
   beside the collapsed nav; below that the callbar carries the address instead,
   which is stronger anyway because it is tappable for directions. */
.masthead-where {
  display: none;
  margin-left: auto;
  align-items: center;
  gap: 7px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  white-space: nowrap;
  color: rgba(20, 17, 14, 0.58);
  transition: color var(--dur-ui) var(--ease);
}

.masthead-where svg {
  color: var(--ember);
  flex-shrink: 0;
}

.masthead-where:hover {
  color: var(--on-paper);
}

@media (min-width: 1024px) {
  .masthead-where {
    display: inline-flex;
  }
}

/* The phone, kept but quieted.
   
   It was a bordered 38px pill — the same visual weight as the bag and the
   search icon beside it. On a page that sells online as well as by phone, the
   number should not out-shout the shopping controls.
   
   A rule under the digits instead of a box around them: still unmistakably a
   link, no longer a button. The number stays in the bar because this business
   genuinely converts on calls; only its volume changes.
   
   NOTE: an identical override was appended to chrome.css first and did
   nothing, because this rule lives in deviceland.css, which loads later and
   won. Same trap as the radius tokens — chrome.css and deviceland.css both
   describe this component, so a change has to be made where the winning copy
   lives. */
.masthead-call {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 38px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: var(--ink-3);
  transition: color var(--dur-ui) var(--ease);
}

.masthead-call svg {
  color: var(--ember-deep);
}

/* No underline. De-boxing it was right — it had the same weight as the bag —
   but replacing the box with a rule just swapped one piece of decoration for
   another, and in a masthead an underlined string reads as an unstyled default
   link. It also added a fourth horizontal line to a header that already has
   the folio's dividers and its bottom rule.
   
   The ember phone glyph is the affordance. Nothing else is needed. */
.masthead-call:hover {
  color: var(--ember-deep);
}

/* Phone-width masthead is logo + menu only; the callbar owns the phone number
   down there and owns it better, because it never scrolls away. */
@media (max-width: 599px) {
  .masthead-call span {
    display: none;
  }
  .masthead-call {
    padding: 0 11px;
  }
}

.masthead-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 2px;
}

/* When the address is present it owns the auto margin, so the actions sit
   directly beside it instead of being pushed to the far edge.

   GATED TO >=1024px, and this was the bug behind every "hamburger outside the
   island" report. `~` matches on DOM order, not on display, and
   .masthead-where is display:none below 1024px — so on a phone this rule still
   applied, replaced `margin-left: auto` with a fixed 22px, and stopped the
   actions being right-aligned at all.

   Measured at 375px: .masthead-inner ran 4..371 with 24px padding, so the
   content box ended at 347, and .masthead-actions reported L232 R396 — 49px
   past it. Every island inset I tuned was downstream of that, which is why
   twelve attempts at the geometry never landed. */
@media (min-width: 1024px) {
  .masthead-where ~ .masthead-actions {
    margin-left: 22px;
  }
}

/* ------------------------------------------------------------
   Phone masthead — every override in ONE place, declared last
   ------------------------------------------------------------

   Placed here, after all the base masthead rules, on purpose. Three separate
   rules in this file have already been defeated by source order rather than by
   specificity — the icon-btn shrink queries, and the pair below — because they
   were written next to the other mobile rules near the top while their base
   declarations appear further down. At equal specificity the later declaration
   wins whether or not the earlier one sits in a media query.

   Anything that has to beat a base masthead rule belongs in this block.

   The geometry is now symmetric by construction rather than by arithmetic:
   .masthead-inner is the pill (inset 4px from the viewport), its own 24px
   padding is the clearance, and both the logo and the actions sit flush to
   that padding. Left inset and right inset are the same number, and it tracks
   --gut. Nothing here needs re-deriving when a control is added or an asset
   changes width.
   ------------------------------------------------------------ */
@media (max-width: 767px) {
  /* The call button goes, which is what the note beside .masthead-call has
     said all along: the call bar owns the phone number on a phone and owns it
     better, because it never scrolls away. It also buys ~45px, taking the row
     from 12px over to roughly 49px of slack at 375px — the difference between
     a layout that just fits and one that keeps fitting. */
  .masthead-call {
    display: none;
  }

  /* Restores right-alignment that the sibling rule above was overriding. */
  .masthead-where ~ .masthead-actions {
    margin-left: auto;
  }

  /* The 16px and 12px inboard nudges exist to clear the pill's radius on
     desktop, where the island is inset 24px inside the content box. On a phone
     the pill IS the content's box, so its padding is already the clearance and
     these only push the controls toward the cap they were meant to avoid. */
  .masthead.is-stuck .logo {
    margin-left: 0;
  }

  .masthead.is-stuck .masthead-actions,
  .masthead-actions {
    padding-right: 0;
  }
}

/* Chrome components carry their own reset.
   ------------------------------------------------------------
   Audited after the third time chrome.css silently lost something. The
   generator strips every bare element selector on purpose — that reset landing
   on Elessi's markup is what broke the product page early on — so all of these
   from the top of this file never reach a WooCommerce page:

     html      scroll-padding-top
     body      margin, background, colour, font, overflow-x
     img, svg  display:block, max-width
     a         color:inherit, text-decoration:none
     button    the full control reset
     ul        margin, padding, list-style
     h1-h3, p, dl, dd, dt   margin: 0

   Three of those matter to the chrome and were live defects, not risks: links
   in the masthead, nav sheet and footer were taking Elessi's link colour and
   underline; every footer list was showing bullets and a default indent; and
   footer paragraphs and definition lists were carrying UA margins.

   Scoping the same declarations to the components fixes them and, more
   usefully, means the next component added to chrome.css inherits a sane base
   instead of depending on a reset that will not be there. */
/* text-decoration only. NO color.
   ------------------------------------------------------------
   `color: inherit` here was the same trap as the button reset: `.callbar a` is
   (0,1,1) and `.callbar-call { color: var(--on-ember) }` is (0,1,0), so the
   reset won and the call bar's label went from white to the dark colour it
   inherited from its parent. It was outranking .foot-col a, .nav a and every
   .btn-* variant sitting on an anchor for the same reason.

   Colour is declared by the components, which already do it — .nav a,
   .foot-col a, .callbar-call and .callbar-btn all set their own. The few plain
   links that relied on inheritance are given it explicitly below, so nothing
   depends on a blanket rule that can only be too strong or too weak. */
.masthead a,
.nav-sheet a,
.callbar a,
.foot a {
  text-decoration: none;
}

/* Plain links with no colour of their own. Without these, Elessi's `a` rules
   reach them on a WooCommerce page, where chrome.css carries no element
   reset. */
.nav-sheet-nav a,
.foot-legal a,
.footnotes a,
.foot-bar a {
  color: inherit;
}

.masthead ul,
.nav-sheet ul,
.callbar ul,
.foot ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.foot p,
.foot h1,
.foot h2,
.foot h3,
.foot dl,
.foot dd,
.foot dt,
.nav-sheet p {
  margin: 0;
}

.masthead svg,
.nav-sheet svg,
.callbar svg,
.foot svg {
  display: block;
  max-width: 100%;
}

/* Chrome buttons carry their own reset.
   ------------------------------------------------------------
   This file resets bare `button` near the top, and that was enough while the
   only consumer was the homepage. It is not enough for a WooCommerce page:
   chrome.css is generated from this file with every bare element selector
   stripped on purpose — that reset landing on Elessi's markup is what broke
   the product page once already — so `button` never made it across.

   The result was a menu toggle wearing Elessi's default button styling on the
   product page: an ember fill instead of a transparent hit area, and, more
   damagingly, Elessi's button padding, which widened it far beyond 38px and
   pushed it past the island and off the side of the screen. Same component,
   two appearances, and the overflow only on the page missing the reset.

   Declaring it on the components themselves means it survives extraction and
   both pages get one button. */
/* Deliberately does NOT set width or color.
   ------------------------------------------------------------
   `.masthead button` is specificity (0,1,1) and `.icon-btn` is (0,1,0), so
   every property declared here outranks the component that follows. The first
   version of this reset included `width: auto` and `color: inherit`, and both
   won:

     width  the menu toggle collapsed from 36px to 18px — the raw size of its
            own SVG — measured as #nav-toggle W18 against .icon-btn's 36. It
            hit only the hamburger because that is the one <button> in the
            masthead; Account and Bag are anchors and kept their size, which is
            exactly why the hamburger looked cramped and misaligned next to
            them, and why its tap target was half the size it should be.

     color  the toggle inherited ink while its neighbours used the muted grey,
            so one icon sat darker than the other three.

   A reset that outranks its own components is a trap. This one is limited to
   properties no chrome component declares. */
.masthead button,
.nav-sheet button,
.callbar button,
.foot button,
.icon-btn {
  font: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  box-shadow: none;
  text-transform: none;
  letter-spacing: inherit;
  line-height: normal;
  min-height: 0;
}

.icon-btn {
  position: relative;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  flex-shrink: 0;
  border-radius: var(--r-pill);
  color: rgba(20, 17, 14, 0.66);
  transition: background 0.3s var(--ease), color 0.3s var(--ease);
}

.icon-btn:hover {
  background: rgba(20, 17, 14, 0.07);
  color: var(--on-paper);
}

/* Control shrink, declared AFTER the base .icon-btn above.
   ------------------------------------------------------------
   These were written near the masthead media queries at the top of the file,
   where they did nothing: .icon-btn's base rule appears later in the source,
   and at equal specificity source order decides regardless of media queries.
   The generated chrome.css made it visible — 36px and 34px both listed before
   the 38px base.

   375px leaves only a few pixels of slack once the wordmark, the gap and four
   controls are accounted for, so 36px buys back the margin needed to absorb a
   rounding difference or a wider bag badge. 34px is for the 320px class of
   phone, where the call button also goes. */
@media (max-width: 420px) {
  .icon-btn {
    width: 36px;
    height: 36px;
  }
}

@media (max-width: 359px) {
  .icon-btn {
    width: 34px;
    height: 34px;
  }
}

/* REMOVED, and worth recording why.

   This rule hid .icon-btn:nth-child(1) and (2) below 560px, back when
   .masthead-actions held four buttons — Search, Account, Bag, Menu — and the
   first two were placeholders that duplicated the sheet.

   Search, Account and Bag were later deleted as dead controls and replaced by a
   single .masthead-call link. That left two children: the call link and the menu
   button. The call link is an <a> and never matched .icon-btn, so nth-child(2)
   silently retargeted from "Account" to the HAMBURGER — and mobile shipped with
   no navigation at all.

   A positional selector outlived the children it was counting. The lesson is to
   hide things by what they are, not by where they sit; nothing here needs hiding
   now, since both remaining controls do something. */

/* ============================================================
   Find — search overlay and sheet field
   ============================================================

   Declared AFTER .icon-btn above, deliberately. .masthead-search is (0,1,0)
   and so is .icon-btn, so `display: none` only wins here on source order.
   Four rules in this file have already lost that way.
   ============================================================ */

.masthead-search {
  display: none;
}

@media (min-width: 1024px) {
  .masthead-search {
    display: grid;
  }
}

/* ---------- the overlay ---------- */

/* 900 sits above the masthead at 800 and the nav sheet at 880, and below the
   skip link at 999, which must stay reachable. */
.find-overlay {
  position: fixed;
  inset: 0;
  z-index: 900;
  display: grid;
  align-content: start;
  justify-items: center;
  padding: 11vh 24px 24px;
  opacity: 0;
  transition: opacity var(--dur-ui) var(--ease);
}

/* MANDATORY, and its absence took the whole site down.
   ------------------------------------------------------------
   `hidden` only hides an element because of a UA rule, [hidden] { display:
   none }, and any author `display` declaration outranks it. The rule above
   sets display: grid, so the overlay stayed laid out while hidden: fixed,
   inset 0, z-index 900, opacity 0. An invisible full-viewport sheet of glass
   over every page — every button on the site stopped responding, and nothing
   looked wrong.

   .nav-sheet already carries this exact guard eleven hundred lines up. Its
   base rule declares no display, so [hidden] would have worked there without
   it; the guard was written anyway. I added the fixed overlay and not the
   guard, which is the one difference that mattered. */
.find-overlay[hidden],
.find-results[hidden] {
  display: none;
}

.find-overlay.is-open {
  opacity: 1;
}

.find-scrim {
  position: fixed;
  inset: 0;
  background: rgba(20, 17, 14, 0.44);
  backdrop-filter: blur(7px) saturate(1.2);
  -webkit-backdrop-filter: blur(7px) saturate(1.2);
}

.find-panel {
  position: relative;
  width: 100%;
  max-width: 620px;
  background: var(--paper);
  border: 1px solid var(--hair-light);
  border-radius: var(--r-lg);
  box-shadow: var(--e4);
  overflow: hidden;
  transform: translateY(-10px) scale(0.985);
  transition: transform var(--dur-reveal) var(--ease);
}

.find-overlay.is-open .find-panel {
  transform: none;
}

.find-field {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 16px 0 20px;
  height: 62px;
  border-bottom: 1px solid var(--hair-light);
}

.find-field-icon,
.find-sheet-icon {
  flex: 0 0 auto;
  color: var(--muted-fg);
}

/* -webkit-appearance is reset because type="search" gets a UA inner
   spin/cancel button and its own font in Safari. */
.find-input,
.find-sheet-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: none;
  outline: none;
  font-family: var(--sans);
  font-size: 16px;
  font-weight: 450;
  letter-spacing: -0.011em;
  color: var(--on-paper);
  -webkit-appearance: none;
  appearance: none;
}

.find-input::placeholder,
.find-sheet-input::placeholder {
  color: rgba(20, 17, 14, 0.42);
}

.find-input::-webkit-search-decoration,
.find-input::-webkit-search-cancel-button,
.find-sheet-input::-webkit-search-decoration,
.find-sheet-input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
}

.find-esc {
  flex: 0 0 auto;
  padding: 5px 9px;
  border: 1px solid var(--hair-light-2);
  border-radius: var(--r-sm);
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--muted-fg);
  background: none;
  cursor: pointer;
  transition: color var(--dur-micro) var(--ease),
    border-color var(--dur-micro) var(--ease);
}

.find-esc:hover {
  color: var(--on-paper);
  border-color: var(--hair-light-2);
}

/* ---------- results, shared by overlay and sheet ---------- */

.find-results {
  max-height: min(56vh, 460px);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.find-item {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 20px;
  border-bottom: 1px solid var(--hair-light);
  color: inherit;
  text-decoration: none;
  transition: background var(--dur-micro) var(--ease);
}

.find-item:last-of-type {
  border-bottom: 0;
}

.find-item:hover,
.find-item.is-active {
  background: var(--paper-2);
}

/* White, not the warm plate. Apple's catalogue shots are cut out on pure
   white, so a cream tile draws a visible rectangle around every cable — the
   same call already made for the gallery and the related-product cards. */
.find-item-img {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  background: #fff;
  border: 1px solid var(--hair-light);
  border-radius: var(--r-sm);
  overflow: hidden;
}

.find-item-img img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.find-item-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.find-item-body b {
  font-size: 14.5px;
  font-weight: 500;
  letter-spacing: -0.008em;
  line-height: 1.3;
}

.find-item-sku {
  font-size: 11.5px;
  font-style: normal;
  color: var(--muted-fg);
}

/* Never shrinks, and the name wraps instead — the same fix the homepage cards
   needed after prices were measured hanging 13px off a 375px screen. */
.find-item-price {
  flex: 0 0 auto;
  font-size: 14.5px;
  font-weight: 500;
  letter-spacing: -0.01em;
  text-align: right;
}

.find-item-price del {
  display: none;
}

/* Caption under the price, in the same muted voice as the SKU: a fact,
   not an alert. Mirrored in chrome.css, which the homepage does not load. */
.find-item-state {
  display: block;
  margin-top: 2px;
  font-size: 11.5px;
  font-weight: 400;
  color: var(--muted-fg);
}

.find-item-price ins {
  text-decoration: none;
}

.find-all {
  display: block;
  padding: 13px 20px;
  border-top: 1px solid var(--hair-light);
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--muted-fg);
  text-decoration: none;
  transition: color var(--dur-micro) var(--ease);
}

.find-all:hover {
  color: var(--on-paper);
}

/* ---------- nothing found ---------- */

/* An empty result on a six-family catalogue reads as "they do not stock it",
   not as "check your spelling". So it answers with the one action that can
   still make the sale: the counter has stock and knowledge the website does
   not list. */
.find-empty {
  padding: 26px 20px 24px;
  text-align: center;
}

.find-empty p {
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--on-paper);
}

.find-empty .btn {
  margin-top: 14px;
}

.find-empty-note {
  margin-top: 12px;
  font-size: 12px;
  color: var(--muted-fg);
}

/* ---------- keyboard hint ---------- */

/* Pointer-only devices never see this: a phone has no arrow keys, and a hint
   about them is noise on the surface with the least room for noise. */
.find-hint {
  display: none;
  gap: 16px;
  padding: 11px 20px;
  border-top: 1px solid var(--hair-light);
  background: var(--paper-2);
  font-size: 11px;
  color: var(--muted-fg);
}

@media (pointer: fine) {
  .find-hint {
    display: flex;
  }
}

.find-hint b {
  display: inline-grid;
  place-items: center;
  min-width: 17px;
  height: 17px;
  margin-right: 2px;
  padding-inline: 4px;
  border: 1px solid var(--hair-light-2);
  border-radius: 2px;
  font-size: 10px;
  font-weight: 500;
}

/* ---------- the sheet field ---------- */

.find-sheet {
  display: flex;
  align-items: center;
  gap: 11px;
  height: 52px;
  padding-inline: 16px;
  margin-bottom: 18px;
  background: var(--paper-2);
  border: 1px solid var(--hair-light);
  border-radius: var(--r-pill);
}

.find-sheet:focus-within {
  border-color: var(--hair-light-2);
  background: var(--paper);
}

.find-results-sheet {
  margin-bottom: 18px;
  background: var(--paper);
  border: 1px solid var(--hair-light);
  border-radius: var(--r-md);
  overflow: hidden;
  overflow-y: auto;
}

/* Typing replaces the category list rather than pushing it down. Two stacked
   lists of links, one of them stale, is the worst of both. */
.nav-sheet.find-active .nav-sheet-nav {
  display: none;
}

/* ============================================================
   Bag drawer
   ============================================================

   Contents are WooCommerce's mini-cart markup, so every selector below is a
   vanilla Woo class. That is the whole reason this was built rather than
   Elessi's drawer restyled: .mini_cart_item and
   .woocommerce-mini-cart__buttons are stable, documented and server-rendered,
   where .nasa-static-sidebar is filled from JS templates.
   ============================================================ */

/* FIRST, and not optional. `hidden` is only honoured by a UA rule that any
   author `display` outranks, and .dl-drawer declares display: grid — the same
   omission on the search overlay put an invisible full-viewport sheet of glass
   over the site and killed every click on every page. */
.dl-drawer[hidden] {
  display: none;
}

.dl-drawer {
  position: fixed;
  inset: 0;
  z-index: 910;
  display: grid;
  justify-items: end;
}

.dl-drawer-scrim {
  position: fixed;
  inset: 0;
  background: rgba(20, 17, 14, 0.44);
  backdrop-filter: blur(7px) saturate(1.2);
  -webkit-backdrop-filter: blur(7px) saturate(1.2);
  opacity: 0;
  transition: opacity var(--dur-ui) var(--ease);
}

.dl-drawer.is-open .dl-drawer-scrim {
  opacity: 1;
}

/* min() rather than a breakpoint: on a phone it is the full width less a
   thumb's worth of scrim, so the page behind stays visible and tappable as an
   escape route. */
.dl-drawer-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(420px, calc(100vw - 44px));
  height: 100%;
  background: var(--paper);
  border-left: 1px solid var(--hair-light);
  box-shadow: var(--e4);
  transform: translateX(100%);
  transition: transform var(--dur-reveal) var(--ease);
}

.dl-drawer.is-open .dl-drawer-panel {
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .dl-drawer-panel {
    transition: none;
  }
}

.dl-drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 20px;
  border-bottom: 1px solid var(--hair-light);
}

.dl-drawer-title {
  margin: 0;
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.5rem;
  line-height: 1.1;
  letter-spacing: -0.015em;
  color: var(--on-paper);
}

.dl-drawer-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 4px 20px;
}

/* The replaceable node owns no layout: WooCommerce swaps this element wholesale
   on every fragment refresh, so anything declared on it is temporary. */
.dl-drawer-body > .widget_shopping_cart_content,
.dl-drawer .nasa-minicart-items {
  display: block;
  padding: 0;
  margin: 0;
}

.dl-drawer-note {
  margin: 0;
  padding: 12px 20px 18px;
  font-size: 11.5px;
  color: var(--muted-fg);
}

/* ---------- WooCommerce mini-cart ---------- */

.dl-drawer .woocommerce-mini-cart,
.dl-drawer ul.cart_list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* THE LAYOUT IS DECLARED HERE, not inherited from Elessi's utilities.
   ------------------------------------------------------------
   Elessi filters WooCommerce's mini-cart heavily. The item is not the
   documented <li><a><img>name</a> — it is

     .mini_cart_item.nasa-flex
       .nasa-image-cart-item > img
       .nasa-info-cart-item.padding-left-15
         .mini-cart-info
           .nasa-flex.jbw  > a.product-name + span.product-remove
           dl.variation
           .nasa-flex.jbw.mini-cart-item-price > .quantity-wrap + price

   and the layout comes from nasa-flex, jbw, align-start and padding-left-15 —
   Elessi utility classes defined in the parent stylesheet, WHICH THE HOMEPAGE
   DEQUEUES. So relying on them would give a flex row on shop pages and an
   unstyled stack of blocks on the homepage, from the same markup.

   Declaring the flex here against Elessi's structural children means the
   drawer is identical on every page whether that stylesheet is present or not.
   padding-left-15 is neutralised in favour of gap for the same reason. */
.dl-drawer .mini_cart_item {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin: 0;
  padding: 16px 0;
  border-bottom: 1px solid var(--hair-light);
  list-style: none;
}

.dl-drawer .nasa-image-cart-item {
  flex: 0 0 56px;
}

.dl-drawer .nasa-image-cart-item img,
.dl-drawer .mini_cart_item > a img {
  width: 56px;
  height: 56px;
  object-fit: contain;
  padding: 5px;
  background: #fff;
  border: 1px solid var(--hair-light);
  border-radius: var(--r-sm);
}

.dl-drawer .nasa-info-cart-item {
  flex: 1 1 auto;
  min-width: 0;
  padding-left: 0;
  padding-right: 0;
}

.dl-drawer .mini-cart-info > .nasa-flex {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}

.dl-drawer .product-name,
.dl-drawer .mini_cart_item > a:not(.remove) {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -0.006em;
  line-height: 1.35;
  color: var(--on-paper);
  text-decoration: none;
}

.dl-drawer .product-name:hover {
  color: var(--ember-deep);
}

.dl-drawer dl.variation {
  margin: 5px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted-fg);
}

/* Values only, labels clipped — the same spec line the product page and the
   search results use. A <dt> and its <dd> are separate inline elements, so a
   line break can fall between a label and the value it belongs to, and the
   labels restate what each value already says: 16GB is memory, 1TB SSD is
   storage. Clipped rather than display:none so the pairing survives for a
   screen reader. */
.dl-drawer dl.variation dt {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.dl-drawer dl.variation dd {
  display: inline;
  margin: 0;
  font-weight: 400;
}

.dl-drawer dl.variation dd p {
  display: inline;
  margin: 0;
}

.dl-drawer dl.variation dd::after {
  content: "\00a0·\00a0";
}

.dl-drawer dl.variation dd:last-of-type::after {
  content: "";
}

.dl-drawer .mini-cart-item-price {
  align-items: center;
  margin-top: 9px;
}

.dl-drawer .mini-cart-item-price .amount {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--on-paper);
}

/* The × is drawn here rather than left to the icon font.
   Elessi's remove anchor is EMPTY and gets its glyph from .nasa-stclose in
   elessi-fonts-icons — which the homepage dequeues, so the button would have
   been an invisible 22px hole on the page with the most traffic. font-size: 0
   suppresses the glyph where the font IS loaded, so it cannot double up. */
/* Everything Elessi could be drawing here is stripped, not just the glyph.
   ------------------------------------------------------------
   A short horizontal bar was appearing above the x. Suppressing ::after and
   zeroing font-size removed the second pseudo-element and the icon-font
   character, but a mark drawn as a BORDER or a background-image survives both —
   and .nasa-stclose is styled in several of Elessi's stylesheets, so which one
   applies depends on the page.

   Rather than identify which, the anchor is reduced to a box that can only draw
   what this rule gives it: no border, no background image, no shadow, no text
   decoration, and overflow hidden so nothing can spill past its 24px. */
.dl-drawer .dl-drawer-panel .product-remove a.remove,
.dl-drawer .dl-drawer-panel a.remove_from_cart_button,
.dl-drawer .product-remove a.remove,
.dl-drawer a.remove_from_cart_button {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  background-image: none;
  box-shadow: none;
  color: rgba(20, 17, 14, 0.4);
  font-size: 0;
  line-height: 1;
  text-decoration: none;
  text-indent: 0;
  overflow: hidden;
  transition: color var(--dur-micro) var(--ease),
    background var(--dur-micro) var(--ease);
}

/* Drawn as a MASK, not typed as a character.
   ------------------------------------------------------------
   The previous version set content: "\00d7" — a multiplication sign — and it
   rendered as a dash above a chevron. The anchor carries .nasa-stclose, and
   Elessi's icon font is active on it, so U+00D7 was being resolved against a
   glyph table where that codepoint is some other symbol entirely. A character
   is only as reliable as the font that happens to win.

   An SVG mask has no font to lose to: the shape is the stylesheet's, painted in
   currentColor, and it cannot be substituted, ligatured or remapped. Same
   technique as the step-indicator chevron. */
.dl-drawer .dl-drawer-panel .product-remove a.remove::before,
.dl-drawer .dl-drawer-panel a.remove_from_cart_button::before,
.dl-drawer .product-remove a.remove::before,
.dl-drawer a.remove_from_cart_button::before {
  content: "";
  width: 11px;
  height: 11px;
  background: currentColor;
  transform: none;
  -webkit-mask: no-repeat center / 11px 11px
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M1.5 1.5l9 9M10.5 1.5l-9 9' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  mask: no-repeat center / 11px 11px
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M1.5 1.5l9 9M10.5 1.5l-9 9' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
}

/* Elessi builds its close mark from TWO pseudo-elements — a pair of bars
   rotated into an x. Overriding ::before with a real x character left the
   second bar behind, which is the stray + beside the remove button on the
   product page, where that stylesheet is loaded. */
.dl-drawer .product-remove a.remove::after,
.dl-drawer a.remove_from_cart_button::after {
  display: none;
  content: none;
}

.dl-drawer .product-remove a.remove:hover,
.dl-drawer a.remove_from_cart_button:hover {
  background: var(--paper-2);
  color: var(--ember-deep);
}

/* Same stepper as the cart and the product page. One control, one definition —
   and it works here without Elessi's utilities because .quantity carries its
   own dimensions. */
/* 76x46 with 22px halves — IDENTICAL to pdp.css and bag.css, deliberately.
   pdp.css matches `html body.deviceland-shop .quantity` at (0,2,2) and loads
   after this file, so on a product page its rule wins over `.dl-drawer
   .quantity` at (0,2,0) no matter what is written here. Rather than escalate
   specificity to win a fight over the same control, the numbers are the same on
   every surface — then whichever rule applies, the stepper is identical. A
   drawer opened from a product page and one opened from the homepage were
   otherwise 6px different in height. */
.dl-drawer .quantity {
  position: relative;
  float: none;
  display: inline-block;
  width: 76px;
  height: 46px;
  margin: 0;
  border: 1px solid var(--hair-light-2);
  border-radius: var(--r-sm);
  background: var(--bone);
  overflow: hidden;
}

.dl-drawer .quantity input.qty,
.dl-drawer .quantity input[type="number"] {
  width: 48px;
  height: 44px;
  min-height: 0;
  line-height: 44px;
  padding: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--on-paper);
  text-align: center;
  -moz-appearance: textfield;
  appearance: textfield;
}

.dl-drawer .quantity input[type="number"]::-webkit-outer-spin-button,
.dl-drawer .quantity input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
}

.dl-drawer .quantity .plus,
.dl-drawer .quantity .minus {
  position: absolute;
  right: 0;
  left: auto;
  display: grid;
  place-items: center;
  width: 27px;
  height: 22px;
  min-height: 0;
  padding: 0;
  margin: 0;
  border: 0;
  border-left: 1px solid var(--hair-light-2);
  border-radius: 0;
  background: transparent;
  color: var(--muted-fg);
  text-decoration: none;
}

.dl-drawer .quantity .plus {
  top: 0;
  bottom: auto;
  border-bottom: 1px solid var(--hair-light-2);
}

.dl-drawer .quantity .minus {
  bottom: 0;
  top: auto;
}

.dl-drawer .quantity .plus svg,
.dl-drawer .quantity .minus svg {
  width: 11px;
  height: 11px;
}

.dl-drawer .quantity .plus:hover,
.dl-drawer .quantity .minus:hover {
  background: rgba(20, 17, 14, 0.05);
  color: var(--on-paper);
}

/* The unit price is hidden; the line total stays.
   ------------------------------------------------------------
   Elessi prints both — .cart_list_product_quantity is "× $529.00" beside the
   stepper, and .mini-cart-item-subtotal is the line total on the right. At a
   quantity of one they are the same number twice, which is what read as a
   duplicate. The stepper already shows the count and the right-hand column
   already shows what that count costs, so the middle term is the one to drop. */
.dl-drawer .cart_list_product_quantity {
  display: none;
}

.dl-drawer .mini-cart-item-price {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 9px;
}

.dl-drawer .quantity-wrap {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
}

.dl-drawer .mini-cart-item-subtotal {
  flex: 0 0 auto;
  margin: 0;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--on-paper);
}

/* The footer's layout is declared here, not inherited.
   ------------------------------------------------------------
   Elessi wraps the total and the buttons in .nasa-minicart-footer and lays
   them out with .right, .text-center, .margin-left-10, .nasa-flex and .jbw —
   utility classes from the parent stylesheet, which the homepage dequeues. That
   is why this footer behaved differently on the two pages: on the product page
   .right floated the amount and Elessi's rules positioned the block, and on the
   homepage none of it applied.

   float and position are explicitly neutralised because a float here escaped
   the flow and let the subtotal ride up over the note beneath it. */
.dl-drawer .nasa-minicart-footer {
  position: static;
  float: none;
  width: auto;
  margin: 0;
  padding: 0;
  box-shadow: none;
  background: transparent;
}

.dl-drawer .woocommerce-mini-cart__total,
.dl-drawer .minicart_total_checkout {
  position: static;
  float: none;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin: 0;
  padding: 16px 0;
  border-bottom: 1px solid var(--hair-light);
  font-size: 13.5px;
  text-align: left;
  color: var(--muted-fg);
}

.dl-drawer .woocommerce-mini-cart__total strong {
  font-weight: 400;
}

.dl-drawer .total-price-label {
  flex: 0 0 auto;
}

.dl-drawer .total-price {
  float: none;
  flex: 0 0 auto;
  text-align: right;
}

.dl-drawer .woocommerce-mini-cart__total .amount,
.dl-drawer .total-price .amount {
  font-size: 20px;
  font-weight: 500;
  letter-spacing: -0.02em;
  color: var(--on-paper);
}

.dl-drawer .btn-mini-cart {
  position: static;
  float: none;
  width: auto;
  padding: 0;
  text-align: left;
}

/* Ink for checkout, outline for view cart — one filled button per surface, on
   the action that moves forward.
   ------------------------------------------------------------
   SPECIFICITY, and it is deliberately high. The previous version of these rules
   was `.dl-drawer .woocommerce-mini-cart__buttons a` at (0,2,1) and it was
   present in the served chrome.css, correct, and losing: the buttons still came
   out ember, uppercase and square. Elessi reaches these through
   `.btn-mini-cart .button` and its own longer chains, and dynamic.css restates
   the accent, so anything at (0,2,1) is inside the range where it is a coin
   toss decided by print order.

   Routing through .dl-drawer-panel and naming .button lifts these clear of that
   band. It is the drawer's own component, so scoping this tightly costs nothing
   and stops the styling depending on which stylesheet happens to print last. */
.dl-drawer .dl-drawer-panel .woocommerce-mini-cart__buttons,
.dl-drawer .dl-drawer-panel .btn-mini-cart {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 16px 0 4px;
}

.dl-drawer .dl-drawer-panel .btn-mini-cart .woocommerce-mini-cart__buttons {
  padding: 0;
}

.dl-drawer .dl-drawer-panel a.button,
.dl-drawer .dl-drawer-panel .woocommerce-mini-cart__buttons a,
.dl-drawer .dl-drawer-panel .btn-mini-cart a.button,
.dl-drawer .dl-drawer-panel .btn-mini-cart .woocommerce-mini-cart__buttons a.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 48px;
  padding: 0 20px;
  border: 1px solid var(--hair-light-2);
  border-radius: var(--r-pill);
  background: transparent;
  box-shadow: none;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  text-decoration: none;
  color: var(--on-paper);
  transition: background var(--dur-micro) var(--ease),
    border-color var(--dur-micro) var(--ease);
}

.dl-drawer .dl-drawer-panel a.button.checkout,
.dl-drawer .dl-drawer-panel .woocommerce-mini-cart__buttons a.checkout,
.dl-drawer .dl-drawer-panel .btn-mini-cart .woocommerce-mini-cart__buttons a.checkout {
  order: -1;
  border-color: var(--ink);
  background: var(--ink);
  color: var(--on-ink);
}

.dl-drawer .dl-drawer-panel a.button.checkout:hover,
.dl-drawer .dl-drawer-panel .woocommerce-mini-cart__buttons a.checkout:hover {
  background: var(--ink-3);
  border-color: var(--ink-3);
  color: var(--on-ink);
}

.dl-drawer .dl-drawer-panel a.button:not(.checkout):hover,
.dl-drawer .dl-drawer-panel .woocommerce-mini-cart__buttons a:not(.checkout):hover {
  background: var(--paper-2);
  color: var(--on-paper);
}

/* ---------- empty bag ---------- */

/* An empty drawer is not an error. Elessi prints a bag glyph, a grey sentence
   and an ember button, which together read like a warning; this is a shopper
   who has not chosen yet. The glyph goes because it is drawn from the icon font
   the homepage dequeues, so it is a square there and a bag on a product page. */
.dl-drawer .dl-drawer-panel .woocommerce-mini-cart__empty-message {
  margin: 0;
  padding: 34px 0 18px;
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.45rem;
  line-height: 1.2;
  letter-spacing: -0.015em;
  color: var(--on-paper);
  text-align: left;
}

.dl-drawer .dl-drawer-panel .woocommerce-mini-cart__empty-message i,
.dl-drawer .dl-drawer-panel .woocommerce-mini-cart__empty-message svg,
.dl-drawer .dl-drawer-panel .nasa-icon-mini-cart,
.dl-drawer .dl-drawer-panel .empty-mini-cart-icon {
  display: none;
}

/* A count, not a dot. A number is inventory; a dot is decoration. */
.bag-count {
  position: absolute;
  top: 4px;
  right: 3px;
  min-width: 16px;
  height: 16px;
  padding-inline: 4px;
  display: grid;
  place-items: center;
  border-radius: var(--r-pill);
  background: var(--ember);
  color: var(--on-ember);
  font-size: 10px;
  font-weight: 500;
  line-height: 1;
  box-shadow: 0 0 0 2px var(--paper);
}

/* ------------------------------------------------------------
   Mobile navigation
   Below the desktop breakpoint the category nav is not hidden, it moves.
   ------------------------------------------------------------ */

.nav-toggle {
  display: grid;
}

@media (min-width: 1200px) {
  .nav-toggle {
    display: none;
  }
}

.nav-toggle .i-x {
  display: none;
}

.nav-open .nav-toggle .i-bars {
  display: none;
}

.nav-open .nav-toggle .i-x {
  display: block;
}

.nav-sheet {
  position: fixed;
  inset: 0;
  z-index: 780;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: rgba(248, 242, 232, 0.97);
  backdrop-filter: blur(26px) saturate(1.5);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
  opacity: 0;
  transition: opacity var(--dur-ui) var(--ease);
}

.nav-sheet[hidden] {
  display: none;
}

.nav-sheet.is-open {
  opacity: 1;
}

.nav-sheet-inner {
  display: flex;
  flex-direction: column;
  min-height: 100%;
  padding-block: calc(var(--head-h-stuck) + 18px) 40px;
}

.nav-sheet-nav {
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--hair-light);
}

.nav-sheet-nav a {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 18px;
  padding: 17px 2px;
  border-bottom: 1px solid var(--hair-light);
  font-family: var(--display);
  font-size: 1.7rem;
  letter-spacing: -0.02em;
  line-height: 1;
  transform: translateY(10px);
  opacity: 0;
  transition: opacity var(--dur-reveal) var(--ease),
    transform var(--dur-reveal) var(--ease), color var(--dur-micro) var(--ease);
  transition-delay: var(--d, 0ms);
}

.nav-sheet.is-open .nav-sheet-nav a {
  opacity: 1;
  transform: none;
}

.nav-sheet-nav a:hover {
  color: var(--ember);
}

.nav-sheet-note {
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: rgba(20, 17, 14, 0.62);
  text-align: right;
}

.nav-sheet-foot {
  margin-top: auto;
  padding-top: 34px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px 22px;
}

.nav-sheet-tel {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(20, 17, 14, 0.62);
}

body.nav-open {
  overflow: hidden;
}

/* ------------------------------------------------------------
   Trust rail — static, replaces the marquee
   Sits at the foot of the hero, on the hero's own ground: assurances are
   the exit note of the opening view, not a band in front of the brand.
   ------------------------------------------------------------ */

.rail {
  position: relative;
  margin-top: auto;
  border-top: 1px solid var(--hair-light);
  background: transparent;
}

.rail-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0;
  padding-block: 13px;
}

.rail-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding-inline: 22px;
  color: rgba(20, 17, 14, 0.62);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

/* An ember pip instead of a rule: separation without another line on the page */
.rail-item + .rail-item::before {
  content: "";
  flex: 0 0 auto;
  width: 3px;
  height: 3px;
  border-radius: var(--r-pill);
  background: rgba(201, 89, 40, 0.5);
  margin-inline-end: 12px;
}

.rail-item svg {
  color: var(--ember);
  flex-shrink: 0;
}

@media (max-width: 720px) {
  /* The third item used to be dropped on small screens to keep the rail to one
     row. It is now "screens in by 3pm, out today" — the single most useful
     sentence on the page for a repair customer. Wrap the rail instead. */
  .rail-inner {
    row-gap: 9px;
  }
  .rail-item {
    padding-inline: 14px;
    letter-spacing: 0.1em;
  }
  .rail-item + .rail-item::before {
    display: none;
  }
}

/* ------------------------------------------------------------
   Packshot — the sealed frame for an Apple-supplied product photo
   ------------------------------------------------------------

   Every rule Apple's channel guidelines place on product photography lands
   inside one rectangle: the bounding box of the photo itself. Nothing may be
   done to what is in that box, and nothing may be laid over it. Outside it
   there is no constraint at all beyond the ground being white, off-white,
   light gray or black.

   So this component is built as a contract with a hard boundary, and the
   boundary is .packshot-well. Read it as two zones:

     the ROOM   .packshot and .packshot-placard. Ours. Mat colour, hairline,
                radius, elevation, hover lift, type, motion — all free, all
                outside the photo.
     the PLATE  .packshot-well and .packshot-img. Apple's. No filter, no
                transform, no transition, no scrim, no overlay, no crop, and
                nothing positioned on top of it. Ever.

   That division is the whole reason this can look considered while staying
   compliant: the craft is spent on the frame, and the photograph is left
   exactly as it was supplied.

   Three specific prohibitions are enforced by construction rather than by
   convention, because all three were live on this page:

     object-fit: contain     .tile-photo and .prod-photo used `cover`, which
                             crops. "Do not omit portions or use a cropped
                             photograph of Apple products unless it is
                             provided by Apple."
     no transform on the img .tile:hover and .prod:hover scaled the photo
                             1.04 and 1.07. "Do not animate or enhance Apple
                             product photos." The hover lift moves the card
                             instead, so the interaction survives intact.
     nothing over the well   .tile-scrim faded a gradient across the lower
                             85% of the photo and the label, title and price
                             sat on top of it. "Do not place type or graphics
                             on an Apple product photo" — they go beside it,
                             which is what .packshot-placard is for.

   If you are adding a rule here and it targets .packshot-well or
   .packshot-img, it is almost certainly not allowed. Style the card. */

/* The mat is the next rung above the band it sits on, and the well is always
   white. That is the whole rule, and it is a rule because picking mat colours
   by eye produced a measurably wrong answer twice.

   Measured off a render at 1440px, --bone as the mat gave:

     band --paper   89.29%  ->  mat --bone  97.28%  (+7.99)  ->  well  100.00%  (+2.72)
     band --paper-2 83.57%  ->  mat --bone  97.28%  (+13.71) ->  well  100.00%  (+2.72)

   In both cases the mat had climbed almost all the way to white and left the
   well a 2.72-point step above it — under the ~2-3 points needed to be
   reliably visible on an uncalibrated display. The mat was doing the whole
   job and the well was invisible inside it, so the photograph had no frame:
   card and plate read as one white block, which is the same failure as a hole
   in the page wearing a different hat.

   One rung up puts the mat near the midpoint instead:

     band --paper   89.29%  ->  mat --surface 95.72%  (+6.43) ->  well 100.00%  (+4.28)
     band --paper-2 83.57%  ->  mat --paper   89.29%  (+5.72) ->  well 100.00%  (+10.71)

   .packshot is used on --paper, so its mat is --surface. .prod sits on the
   --paper-2 floor band, so its mat is --paper — see the note on that rule. */
.packshot {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 14px;
  border-radius: var(--r-tile);
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--hair-light), var(--e2);
  transition: transform var(--dur-reveal) var(--ease),
    box-shadow var(--dur-reveal) var(--ease);
}

/* The lift the tiles used to get by scaling the photograph. Moving the card
   reads as the same affordance and touches nothing inside the plate. */
a.packshot:hover,
a:hover > .packshot {
  transform: translateY(-4px);
  box-shadow: 0 0 0 1px var(--hair-light-2), var(--e3);
}

/* Literal #fff, deliberately not a token.
   This is not a design decision and must not be tuned: Apple ships its
   packshots with a solid white background baked into the image, so any value
   other than pure white puts a visible rectangle around the asset. --bone
   above it and --paper/--paper-2 beneath it are the values that carry the
   warmth; this one matches the file. */
/* grid-template-rows: minmax(0, 1fr) is load-bearing, not tidiness.
   Without it the single implicit row is auto-sized to its content, and a grid
   item's automatic minimum size then lets the image push the row — and with
   it the well — taller than aspect-ratio asked for. Measured on a 3:2 hero
   well holding a 4:3 asset: the well came out 333x269.7, an aspect of 1.235
   against the 1.5 requested, because the contained image's intrinsic height
   won. The frame was silently taking its shape from whichever asset was in
   it, which is the opposite of the point — and in a row of cards it would
   have made every card a different height. A definite row fixes the frame and
   lets the asset letterbox inside it, which is what contain is for.

   min-height: 0 is the other half of the same bug and the part that actually
   caused it. The well is a flex item — .packshot and .prod are both column
   flex containers — and a flex item's default min-height:auto resolves to a
   content-based automatic minimum size, which outranks aspect-ratio. The row
   fix alone left the 3:2 well still measuring 1.235, because the floor was
   coming from the flex layout rather than from the grid inside it. Zeroing it
   lets the ratio hold: re-measured, 333x222 and 711x474 against the requested
   1.5. There is no shrink risk — neither container is height-constrained. */
/* 1:1 and a 4% mat, both measured off the real assets rather than chosen.
   
   Every packshot the feed delivers is a 2560x2560 SQUARE canvas that already
   carries its own clear space. Measured across the four the homepage uses:
   
                        canvas      product box   product fills   own margin
       MacBook Air    2560x2560       1792x1089     70.0% x 42.5%     15.0%
       MacBook Pro    2560x2560       1710x1035     66.8% x 40.4%     16.6%
       iPhone 17 Pro  2560x2560       1344x1665     52.5% x 65.0%     14.5%
       Watch S11      2560x2560       1604x1863     62.7% x 72.8%     13.6%
   
   So "Do not crowd Apple product images" is already satisfied by the asset.
   The mat here is not the clear space — Apple's canvas is. Treating it as
   the clear space stacked three margins on top of each other: the asset's own
   15%, then a 12% mat, then the letterbox from containing a square asset in a
   3:2 well. The hero product came out 158px wide in a 530px well — 29.9%,
   which is not restraint, it is a thumbnail.
   
       3:2 well, 12% mat  ->  29.9% of well   (what shipped first)
       3:2 well,  4% mat  ->  41.1%
       4:3 well,  4% mat  ->  46.9%
       1:1 well,  4% mat  ->  64.4%           <- this
   
   Matching the well to the asset's square aspect removes the letterbox
   entirely, and 4% is an optical inset for the edge case of a tightly cropped
   alternate rather than a second helping of clear space. Effective space
   around the product is still ~18% of the well once the asset's own margin is
   counted, and the product lands at ~22% of viewport width — subordinate to
   the page, which is what the guidelines actually ask for. */
.packshot-well {
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  place-items: center;
  aspect-ratio: 1 / 1;
  min-height: 0;
  padding: 4%;
  border-radius: var(--r-md);
  background: #fff;
}

/* deviceland_packshot() emits a bare <img> for a catalogue attachment (from
   wp_get_attachment_image, which brings its own srcset) and a <picture> for a
   theme stem. The wrapper has to fill the well's content box so the img rule
   below can size against it — and it stays in flow rather than being absolutely
   positioned, unlike .tile > picture and the old .prod-media > picture, because
   `place-items: center` plus the 12% mat is what holds the product off its own
   edges. Taking the image out of flow would defeat both. */
.packshot-well > picture {
  display: block;
  width: 100%;
  height: 100%;
}

/* "Do not crowd Apple product images." The 12% mat above is what keeps the
   product off its own edges; contain is what keeps all of it in frame.
   
   transition and animation are set to none EXPLICITLY, and that turned out to
   matter. Measured in the live cascade, getComputedStyle on this element
   reported `transition: all` — inherited from the sheet's reset, not from any
   rule written here. So the element the comment above claims is never
   animated was in fact primed to transition every property it has, and
   "Do not animate or enhance Apple product photos" is not a rule you get to
   satisfy by intending to. Declaring none closes it at the element rather
   than relying on nobody upstream ever adding a transition.
   min-*: 0 is the other half of the definite-row fix on the well. */
.packshot-img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  min-width: 0;
  min-height: 0;
  object-fit: contain;
  object-position: center;
  transition: none;
  animation: none;
}

/* The placard. A white rectangle with a caption under it reads as a plate; a
   white rectangle on its own reads as a missing image. This is also where the
   guidelines send everything that used to sit on the photo: "Violators and
   other graphics can be placed beside a product image or described in text." */
.packshot-placard {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 0 6px 4px;
}

.packshot-key {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--on-paper);
}

.packshot-note {
  font-size: 12.5px;
  line-height: 1.5;
  text-wrap: pretty;
  color: var(--muted-fg);
}

/* Escape hatches for a non-square asset, should the feed ever deliver one.
   Set on the figure so an aspect swap is a layout decision and never a change
   to the asset — the image stays contained either way, so the worst case is
   letterboxing inside the well, not a crop.
   .packshot-square is gone: square is the default now, because square is what
   Apple ships. */
.packshot-tall .packshot-well {
  aspect-ratio: 4 / 5;
}

.packshot-wide .packshot-well {
  aspect-ratio: 4 / 3;
}

/* ------------------------------------------------------------
   Hero
   ------------------------------------------------------------ */

/* Full-bleed stage. Kluge et al. (2013) isolate large stage space as one of the
   few measurable differentiators of a luxury homepage: the page should look
   carved out of a photograph, not like a product dropped onto a layout. */
/* Full-bleed stage. Kluge et al. (2013) isolate large stage space as one of the
   few measurable differentiators of a luxury homepage: the page should look
   carved out of a photograph, not like a product dropped onto a layout. */
.hero {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 100svh;
  overflow: hidden;
  background: var(--paper);
}

.hero-media {
  position: absolute;
  inset: 0;
  margin: 0;
  pointer-events: none;
  overflow: hidden;
}

/* The scrims are the whole mechanism. The photograph runs edge to edge and the
   copy side is faded back to paper, so type sits on solid ground with no panel
   behind it. A two-column layout removes the need for scrims and, with it, the
   reason the hero had any weight. */
/* Left at the reference's values. An earlier pass strengthened this band after
   measuring the phone button at 4.30:1 and the spec key at 3.88:1 — both of
   which were artefacts of a bad method (luminance percentiles inside one
   screenshot, which for a bordered pill compares the border to the fill rather
   than the glyph to its ground).

   Measured properly — computed text colour against the composited background
   with the glyphs hidden — the mobile hero runs 5.1:1 to 17.3:1 and nothing
   fails. See herocontrast.mjs. Strengthening the veil was muting the photograph
   to solve a problem that did not exist. */
.hero-scrims {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
      to bottom,
      rgba(248, 242, 232, 0.92) 0%,
      rgba(248, 242, 232, 0.72) 34%,
      rgba(248, 242, 232, 0.34) 58%,
      rgba(248, 242, 232, 0.86) 100%
    ),
    radial-gradient(
      66% 56% at 74% 42%,
      rgba(201, 89, 40, 0.13),
      transparent 72%
    );
}

@media (min-width: 1024px) {
  .hero-scrims {
    background: linear-gradient(
        100deg,
        var(--paper) 0%,
        rgba(248, 242, 232, 0.94) 24%,
        rgba(248, 242, 232, 0.6) 43%,
        rgba(248, 242, 232, 0) 64%
      ),
      radial-gradient(
        62% 54% at 76% 44%,
        rgba(201, 89, 40, 0.12),
        transparent 72%
      ),
      linear-gradient(to top, rgba(248, 242, 232, 0.8) 0%, transparent 24%);
  }
}

/* Museum label, set against the frame rather than inside a box. */
.hero-caption {
  position: absolute;
  right: calc(max(0px, (100vw - var(--shell)) / 2) + var(--gut));
  bottom: 108px;
  display: none;
  max-width: 19rem;
  text-align: right;
  pointer-events: auto;
}

@media (min-width: 1024px) {
  .hero-caption {
    display: block;
  }
}

/* Evidence, not a second hero image: large enough to register as a real
   building at a glance, small enough to stay a caption. */
.hero-caption-shot {
  display: block;
  width: 148px;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  margin: 0 0 14px auto;
  border-radius: var(--r-sm);
  filter: var(--grade-real-sun);
  box-shadow: 0 0 0 1px rgba(20, 17, 14, 0.08), var(--e3);
}

.hero-caption-key {
  display: block;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: rgba(20, 17, 14, 0.72);
}

.hero-caption-txt {
  display: block;
  margin-top: 9px;
  font-size: 12.5px;
  line-height: 1.55;
  text-wrap: pretty;
  color: rgba(20, 17, 14, 0.7);
}

.hero-grid {
  position: relative;
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: 1fr;
  align-content: center;
  padding-block: calc(var(--head-h) + 28px) 44px;
}

/* 448px measure. The break is forced, not hoped for: "Built for / brilliance."
   at 0.86 leading is the whole typographic argument of the page. */
.hero-copy {
  max-width: 28rem;
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 7px 15px 7px 12px;
  border: 1px solid var(--hair-light);
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(8px);
  box-shadow: var(--sh-1);
  color: rgba(20, 17, 14, 0.78);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

.pip-live {
  width: 6px;
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--ember);
  box-shadow: 0 0 0 0 rgba(228, 113, 60, 0.7);
  animation: pulse 2.8s var(--ease-io) infinite;
}

@keyframes pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(228, 113, 60, 0.55);
  }
  70% {
    box-shadow: 0 0 0 9px rgba(228, 113, 60, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(228, 113, 60, 0);
  }
}

.hero-copy .lede {
  margin-top: 26px;
  max-width: 27rem;
  color: rgba(20, 17, 14, 0.66);
}

.hero-cta {
  margin-top: 34px;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.spec-row {
  margin-top: 52px;
  display: flex;
  align-items: stretch;
  gap: 0;
  border-top: 1px solid var(--hair-light);
  padding-top: 26px;
}

.spec {
  padding-right: 30px;
}

.spec + .spec {
  padding-left: 30px;
  border-left: 1px solid var(--hair-light);
}

.spec-val {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.55rem, 2.2vw, 2.1rem);
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--on-paper);
}

.spec-key {
  margin-top: 9px;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(20, 17, 14, 0.62);
}

@media (max-width: 560px) {
  .spec {
    padding-right: 14px;
  }
  .spec + .spec {
    padding-left: 14px;
  }
  /* Tracking is a luxury of width. Below this the label has to fit on one
     line more than it has to look letterspaced. */
  .spec-key {
    font-size: 9.5px;
    letter-spacing: 0.1em;
  }
}

/* ------------------------------------------------------------
   Hero, plate variant — .hero.hero-plate
   ------------------------------------------------------------

   The stage hero above cannot be made compliant by swapping its file. It is a
   full-bleed product photograph with a scrim drawn across it and the headline,
   lede, buttons and spec row sitting on top — and the photo it is built around
   was produced here rather than supplied by Apple, warm-graded, given cast
   shadows and given a screen image that does not exist. Five separate
   prohibitions, and four of them are the layout rather than the asset.

   This variant inverts where the atmosphere comes from. The ROOM is ours and
   carries everything: warm paper, a low ember wash on the copy side, a paper
   grain, the serif, the rules, the motion. The PLATE is a single .packshot in
   the second column, and nothing happens inside it.

   That is also what the guidelines ask for, which is the part worth noticing.
   Apple product photos "must not dominate your web page. They must always be
   subordinate to your company or store identity and cannot take the place of
   that identity." A small packshot in a large field of Deviceland typography
   is not a concession to the rules — it is the arrangement the rules specify,
   and it is the same arrangement that reads as editorial rather than as retail
   when a print art director does it. */

.hero-plate {
  background: var(--paper);
}

/* Warm light, and it stops before the plate does.
   Both washes are centred on the left third and faded out by 70%, so the
   ember never reaches column two. A tint crossing the packshot would be a
   coloured ground under an Apple asset — "Do not place an Apple product image
   on a busy or textured background" — and the well's own white would clip it
   into a hard-edged rectangle even if it were allowed. */
.hero-room {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(
      54% 46% at 16% 28%,
      rgba(201, 89, 40, 0.075),
      transparent 70%
    ),
    radial-gradient(
      64% 58% at 4% 96%,
      rgba(234, 225, 208, 0.85),
      transparent 72%
    );
}

/* The grain the renders used to supply.
   feTurbulence rather than a file: it is ~200 bytes, it never 404s, and it
   scales to any viewport without a second asset. It sits on .hero-room, which
   is behind the grid and stops at the section edge, so it cannot creep under
   the packshot — texture inside the well would breach the background rule as
   surely as colour would. */
.hero-room::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.038;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
}

.hero-plate .hero-grid {
  z-index: 1;
  grid-template-columns: 1fr;
  gap: 46px;
}

@media (min-width: 1024px) {
  /* 5:6 in favour of the plate column, which lands the packshot at roughly
     34% of the viewport and the product itself — inside a 12% mat — at nearer
     26%. The stage hero ran the machine at about 70%. */
  .hero-plate .hero-grid {
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    gap: 64px;
    align-items: center;
  }

  /* No verified packshot for this machine yet, so the second column holds only
     the shop card. Weighting the split back towards the copy keeps the hero
     composed rather than leaving a 6fr gap where a plate should be. */
  .hero-plate-noshot .hero-grid {
    grid-template-columns: minmax(0, 7fr) minmax(0, 4fr);
  }
}

.hero-stage {
  display: flex;
  flex-direction: column;
  gap: 28px;
  min-width: 0;
}

@media (min-width: 1024px) {
  .hero-stage {
    align-items: flex-end;
  }
}

/* The 3:2 override that used to sit here is gone. The reasoning behind it —
   "the machine this hero fronts is a laptop, so the well should be
   landscape" — was about the product's shape and ignored the asset's. The
   asset is a 2560x2560 square with the laptop occupying the middle 42.5% of
   its height, so a 3:2 well letterboxed the square and threw away a third of
   the available width before the product was drawn. Measured: 29.9% of the
   well against 64.4% once the well matched the asset. Frame the canvas you
   are given, not the object inside it. */

/* 30rem, not 35rem, and the constraint is vertical rather than horizontal.
   A square well is as tall as it is wide, which 35rem did not account for:
   measured on the deployed page at a 1440x940 viewport, the well came out
   532px tall, the card ended at y=740, and neither the shop card nor the
   trust rail appeared anywhere on screen — the hero had grown past one
   viewport and pushed "Apple Premier Partner / walk in / same-day screens"
   below the fold. The plate got bigger by taking the page's proof with it.

   Budget at a 900px viewport, which is the shortest common laptop:
   masthead 96 + grid top pad 28, rail 52, grid bottom pad 44, leaving 680 for
   the grid. The stage column costs the well plus 200 (card padding 28, gap
   14, placard 58, gap 28, and the shop card at 72 once it is a row rather
   than a stack). 680 - 200 = 480px = 30rem.

   But a fixed 30rem only fits the screen it was budgeted against, and the
   first attempt at this proved it: 480px measured fine at 1080 and still
   overflowed by 177px at a 900px window. So the cap is expressed against the
   viewport instead of asserted. The 420px is the rest of the stage column
   added up — grid padding 168, rail 44, card padding 28, gap 14, placard 58,
   gap 28, shop card 72 — so the plate takes whatever is left and never more
   than 30rem:

       viewport 804   -> min(480, 384) = 384   well 356, stage column 563.5
       viewport 984   -> min(480, 564) = 480   well 452

   The plate is the element that yields on a short screen, because the trust
   rail and the shop card are the page's evidence and the plate is a picture
   of something the customer can already see.

   The cap earns its place: measured without it, an 804px viewport takes the
   plate to 480, the stage column to 652 and the hero to 863.5, clipping some
   60px off the rail. With it the hero measures 809.1.

   That residual 5.1px is not the plate, and it is worth writing down so the
   next person does not go looking in the wrong column for it. At this height
   the grid is sized by the COPY side — .hero-copy measures 597.6 against
   .hero-stage's 563.5 — so the headline, lede, buttons and spec row are the
   floor and the plate has already stopped mattering. Tried 430px: the well
   went 356 -> 346 and the hero stayed at 809.1 exactly. Reverted to 420. */
@media (min-width: 1024px) {
  .hero-stage .packshot {
    width: 100%;
    max-width: min(30rem, calc(100svh - 420px));
  }
}

/* No .rise on the packshot, and this is deliberate rather than an omission.
   .rise animates opacity and transform, and a transform on the figure moves
   the photograph inside it — "Do not animate or enhance Apple product photos."
   The copy column and the shop card below both still reveal, so the hero
   still resolves rather than simply appearing; the plate is the one still
   thing in it. Being the LCP element, not animating it is also the faster
   choice, so nothing is being traded here. */

/* The shop, in flow under the plate rather than absolutely placed against the
   frame. There is no photograph filling the section any more for an absolute
   caption to be a caption OF, and this is the page's own evidence — our
   building, our grade, no restrictions — so it belongs in the column with the
   product it vouches for. Child typography is shared with the stage hero's
   .hero-caption-* classes. */
/* Desktop only, matching what .hero-caption already did in the stage hero.
   The plate hero stacks copy, plate and this card on a phone, and a fourth
   block would push the rail well past the fold for a photograph that appears
   again at full size in the Visit chapter. Hidden rather than reordered so
   the two heroes agree about what a phone gets. */
.hero-proof {
  display: none;
  flex-direction: column;
  gap: 0;
  margin: 0;
  max-width: 19rem;
}

/* A row on desktop, not a stack, and this is the other half of the vertical
   budget above. Stacked — 148px photograph, then the key, then two lines of
   caption — this card cost about 236px of column height, which is half a
   square plate. Turned on its side it costs 72px, and the plate gets the
   difference back: 480px of well instead of 316px, so the product is drawn
   at roughly 283px rather than 202px. Same information, same photograph, no
   compromise anywhere except that it now reads left-to-right. */
/* width:100% matters as much as the row does. .hero-stage is a column flex
   container with align-items:flex-end, so without it this card shrink-to-fits
   — it measured 269.9px wide, which left the caption a 148px column, wrapped
   "20790 Hawthorne Blvd..." onto six lines and made the card 172.5px tall.
   Taller than the stack it replaced, for the same words. Matching the plate's
   width gives the caption a 358px measure, two lines, and an ~80px card. */
/* Placement is explicit because this card has THREE children — the picture,
   the key and the caption — and a two-column grid auto-places the third into
   column one, underneath the photograph. That is what kept the card 172.5px
   tall even after it was 480px wide: the caption was living in a 108px column
   and wrapping to six lines while 358px of empty column two sat beside it.
   Widening the card had no effect on the element that was too narrow. */
@media (min-width: 1024px) {
  .hero-proof {
    display: grid;
    grid-template-columns: 108px minmax(0, 1fr);
    grid-template-rows: auto auto;
    align-content: center;
    column-gap: 14px;
    width: 100%;
    max-width: 30rem;
    margin-left: auto;
    text-align: right;
  }

  .hero-proof > picture {
    grid-area: 1 / 1 / 3 / 2;
    align-self: center;
  }

  .hero-proof .hero-caption-key {
    grid-column: 2;
    grid-row: 1;
    align-self: end;
  }

  .hero-proof .hero-caption-txt {
    grid-column: 2;
    grid-row: 2;
    align-self: start;
    margin-top: 6px;
  }
}

/* .hero-caption > picture carries the width in the stage hero and is scoped to
   that class, so the plate hero needs its own: without it the <picture> is an
   inline box with no width and the 148px on the img inside leaves a baseline
   gap under the photograph. */
.hero-proof > picture {
  display: block;
  width: 148px;
  margin: 0 0 14px auto;
}

.hero-plate .hero-caption-shot {
  margin: 0 0 14px auto;
}

@media (min-width: 1024px) {
  .hero-proof > picture {
    width: 108px;
    margin: 0;
  }

  .hero-plate .hero-caption-shot {
    width: 108px;
    margin: 0;
  }
}

/* ------------------------------------------------------------
   Hero, split variant — .hero-split
   ------------------------------------------------------------

   A hard diptych: a terracotta panel carrying the type, a white panel
   carrying the machine, and a light band of facts underneath. The white is a
   full-height plane rather than a card, which is the whole point — the panel
   edge does the work a hairline and a shadow were failing to do, so it reads
   as print rather than as a UI card floating in a field.

   The two things this gets right that the plate variants did not: the product
   is large and confidently placed, and there is a large field of saturated
   colour opposite it. A white plane only reads as deliberate when it has
   something to be opposite. */

/* No background on the masthead, and that is a correction.
   I filled it with --paper plus a hairline to keep the nav legible over the
   terracotta half, and that was pure loss: it made the bar opaque, which
   killed the .island — the inset pill with backdrop-filter: blur(22px)
   saturate(1.6) that blurs the page moving underneath it. The whole
   floating-header effect. (The island is now visible at rest too, not just
   when stuck; see the .island rule.)

   It was also solving a problem that does not exist. .hero-split reserves
   var(--head-h) with margin-top, so at rest the area behind the bar is
   body's own --paper and the nav is already on paper. And onCondense() in
   deviceland.js toggles .is-stuck at scrollY > 12, so the transparent state
   is exposed for twelve pixels of scroll before the island takes over. There
   was nothing to fix. */

/* The per-layout .skip:focus override that was here is gone — it was fixed in
   the base rule instead, since a fixed header covers the same spot on every
   layout. */

/* The hero and its band are one viewport together.
   .hero-split alone had min-height: min(70svh, 616px), which stopped it well
   short of the fold and left a slab of empty paper between the band and the
   Lineup chapter. Stacking them in a flex column of exactly the space below
   the masthead lets the diptych take whatever the band does not, so the band
   always lands on the bottom edge of the viewport and the panels always
   reach it — at any height, with no magic number to maintain. */
/* The masthead clearance lives HERE, not on .hero-split.
   With margin-top still on the panel inside the stack it was counted twice:
   the stack began at y=0, reserved 100svh minus the header, and then the
   panel's own margin pushed its contents down by the header again — so the
   band landed exactly var(--head-h) short of the fold. Measured at an 860px
   viewport: the band bottom came to 764 instead of 860. Reserving the
   clearance on the stack makes the stack span head-h to 100svh, which is
   what it is for. */
/* Measured header, not the token, and the admin bar too.
   ------------------------------------------------------------
   --head-h is a fixed token; the header that is actually standing there is
   the bar plus the folio, and for a logged-in member of staff it is also
   pushed down by WordPress's admin bar. Reserving only the token left the
   hero's first credential line drawn behind the folio rail — "Forty-one years
   on Hawthorne" was clipped in half on a phone while logged in.

   --head-measured is published by deviceland.js from the header's own box and
   falls back to the token before it runs, so nothing regresses if the script
   is slow or absent.

   The admin bar is deliberately NOT added here. WordPress already offsets the
   whole document by its height with html{margin-top}, so adding it again
   opened a 69px band of empty cream between the rail and the panel — against
   23px logged out. Measured both ways; the header's own height is the only
   clearance this needs. */
/* Block flow, not a flex column (Oct 11). As a flex item the hero's height
   was the engine's to negotiate, and Safari settled it shorter than the copy:
   the orange panel and the Shop button ran down over the band. A block box
   is never shorter than its content in any engine, so the hero takes a
   min-height of "viewport below the header, less the band" instead, with the
   band's real height measured by deviceland.js (--hero-band-h). */
.hero-stack {
  display: block;
  margin-top: calc(var(--head-measured, var(--head-h)) + env(safe-area-inset-top, 0px));
}

/* No margin-top: .hero-stack reserves the masthead clearance for the pair. */
.hero-split {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  overflow: hidden;
  background: #fff;
}

/* 37/63, not 50/50.
   An even split reads as a template; the asymmetry is what makes it read as
   a composition. It also demotes the terracotta from "half the page" to a
   block with authority, and hands the difference to the product — at 1512px
   the art panel goes from 756px to 952px, so the machine draws about 26%
   larger for free. */
@media (min-width: 900px) {
  .hero-split {
    grid-template-columns: minmax(0, 37fr) minmax(0, 63fr);
  }
}

/* Ember-deep rather than ember. Measured on --ember (#c95928) nothing small
   passes: the lede lands at 3.43:1 and the eyebrow at 3.09:1 against a 4.5:1
   floor. On --ember-deep (#9c3f18) the same type measures 5.18:1 and 4.55:1.
   The gradient to --ember-deep-2 is there so a 700px field of flat rust does
   not read as a printing error. */
.hero-split-copy {
  display: flex;
  align-items: center;
  padding: 60px var(--gut);
  background: linear-gradient(
    148deg,
    var(--ember-deep) 0%,
    var(--ember-deep) 44%,
    var(--ember-deep-2) 100%
  );
  color: var(--on-ember);
}

@media (min-width: 900px) {
  .hero-split-copy {
    /* A floor only. The height now comes from .hero-stack stretching this
       panel to the fold; this stops it collapsing if the copy is ever short. */
    min-height: 420px;
    padding-block: 56px;

    /* Centred in the panel, NOT aligned to the shell's gutter.
    
       Aligning to the shell was the bug. The rule was
           padding-left: max(--gut, (100vw - --shell)/2 + --gut)
       which anchors the copy to a 1480px container centred in the viewport,
       while the panel it lives in is 37vw. Past 1480 the shell's gutter grows
       without limit and the panel does not keep up, so the copy was shoved
       toward the panel's right edge with an ever-widening void on its left:
    
           viewport   left gap   copy width
             1512        3.7%       443px
             1920       13.5%       390px
             2560       22.7%       307px
             3440       29.7%       193px
    
       The comp sits at 4.7%, flat. Centring the block inside its own panel is
       proportional the whole way up — 4.4% to 1512, 7.2% at 1920 — and needs
       no knowledge of --shell at all. The text stays left-aligned inside the
       block; it is the block that centres. */
    justify-content: center;
    /* A floor, not the inset. The inset comes from .hero-split-inner being a
       percentage of the panel below — doing it in both places double-insets
       and the gap drifts wide again (9% of the viewport at 2560). */
    padding-inline: 24px;
  }

}

/* Depth on the copy panel: vignette, then grain.
   Reading the Humanity build, its dark panel is not one flat fill — it is a
   diagonal gradient, then `shadow-[inset_0_0_150px_rgba(0,0,0,0.3)]`, then a
   6% film grain, in that order. The inset vignette is the layer that does the
   most and the one I had left out; a saturated field with darkened edges
   reads as a lit surface, and the same field flat reads as a swatch.
   
   Grain goes to 5%, up from the 3.8% on --paper. Its reference runs 6% on
   near-black and only 1.5% over the photograph, which is worth noting for a
   separate reason: it does not grain its product image either. So confining
   noise to the copy side is not a compromise forced on us by Apple's rules —
   it is what the reference does anyway.
   
   Both layers are on ::after with the copy lifted above them, because a
   positioned pseudo-element otherwise paints over in-flow text. */
.hero-split-copy {
  isolation: isolate;
}

/* Grain only. opacity fades everything on the element it is set on, so the
   vignette cannot share this pseudo-element or it would render at 5% too. */
.hero-split-copy::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.05;
  background-image: var(--grain);
}

/* Vignette only, at full strength. */
.hero-split-copy::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  box-shadow: inset 0 0 160px rgba(38, 12, 0, 0.42);
}

.hero-split-inner {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 30rem;
}

/* 78% of the panel rather than a fixed 30rem, and this override has to live
   HERE — after the base rule — not up in the earlier min-width:900px block
   where I first put it. Both selectors are a single class, so at equal
   specificity source order decides, and the base rule below the media query
   was silently winning: the inner measured 480px at 1479, 1887 and 2527 alike,
   which is exactly what the fix was supposed to stop.
   
   Why a percentage: a fixed inner centred in a growing panel means the side
   gaps grow with the panel — 480px of copy inside a 935px panel is 227px a
   side, 9% of the viewport. A percentage holds it flat at roughly 5% from 1479
   through 2527, which is the comp's 4.7%.
   
   Nothing reflows: the h1's break is forced with <br> and .lede keeps its own
   24rem measure, so the extra width is breathing room, not longer lines. */
@media (min-width: 900px) {
  .hero-split-inner {
    max-width: 78%;
  }
}

/* Tracking to 0.34em, up from 0.17em. Measured against the reference, its
   eyebrow runs tracking-[0.5em] at 10px and its logo 0.2em — the extreme
   letterspacing on very small caps is a large part of why that page reads
   expensive, and it costs nothing. 0.34em rather than 0.5em because this
   label carries a pin glyph and real words rather than two, and at 0.5em
   "TORRANCE · MON–FRI 9–6" stops scanning as a phrase. */
.hero-split-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  /* 86% measures 5.18:1 on --ember-deep. 78% also clears at 4.55:1 but with
     no margin for the gradient's darker end. */
  color: rgba(255, 249, 245, 0.86);
}

.hero-split-inner .h-xl {
  margin-top: 30px;
  font-size: clamp(3rem, 6.6vw, 5.8rem);
}

/* The value word in --sand, italic. 4.88:1 on --ember-deep, well clear of the
   3:1 large-text floor, and warm enough to read as a second voice rather than
   as dimmed white. */
.hero-split-inner .h-xl em {
  font-style: italic;
  color: var(--sand);
}

/* Lighter, airier, and pulled back.
   The reference sets its lede at text-white/50, font-light, leading-[1.8] and
   max-w-md. Ours was 88% at normal weight and default leading, which is a
   paragraph rather than a caption and is why the copy side read as dense
   without reading as considered.

   80%, and the number is measured rather than eyeballed. Against the lighter
   end of the panel gradient (#9c3f18) the alpha ladder runs:

       0.88  5.36    0.80  4.71  <- chosen
       0.82  4.88    0.78  4.55  <- AA floor lands here
                     0.76  4.40  FAIL
                     0.50  2.78  FAIL

   So the reference's own text-white/50 would fail outright here: light grey
   on near-black has headroom that warm white on terracotta does not, and
   copying the value rather than the intent would have shipped a 2.78:1 lede.
   0.80 is one step clear of the floor. The vignette only helps — it darkens
   the ground to rgb(106,42,14) at the edges, where the same alpha measures
   6.63:1 — so the un-vignetted centre is the worst case and that is what the
   ladder above is against. */
.hero-split-inner .lede {
  margin-top: 26px;
  max-width: 24rem;
  font-weight: 300;
  line-height: 1.75;
  color: rgba(255, 249, 245, 0.8);
}

/* .hero-split-rule is no longer emitted — the hairline between the lede and
   the buttons was one more horizontal in a column that already had five.
   The rule is kept for the shop templates that still use the class. */
.hero-split-rule {
  height: 1px;
  margin: 30px 0 0;
  border: 0;
  background: rgba(255, 249, 245, 0.26);
}

.hero-split-eyebrow-dot {
  opacity: 0.55;
}

.hero-split-copy .hero-cta {
  margin-top: 34px;
}

/* The secondary action, as a link rather than a second pill.
   Underline on the label only, not the arrow, so the arrow can translate on
   hover without dragging a rule with it. 86% measures 5.18:1 on
   --ember-deep; the underline sits at 40% because a full-strength rule under
   10px type reads heavier than the type. */
.hero-split-link {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin-top: 20px;
  font-size: 13px;
  font-weight: 500;
  color: rgba(255, 249, 245, 0.86);
  transition: color var(--dur-ui) var(--ease);
}

.hero-split-link span {
  border-bottom: 1px solid rgba(255, 249, 245, 0.4);
  padding-bottom: 2px;
}

.hero-split-link svg:last-child {
  transition: transform var(--dur-ui) var(--ease);
}

.hero-split-link:hover {
  color: var(--on-ember);
}

.hero-split-link:hover span {
  border-bottom-color: var(--on-ember);
}

.hero-split-link:hover svg:last-child {
  transform: translateX(3px);
}

/* 0.60, not the 0.42 this started at. A button border is a non-text UI
   boundary and wants 3:1; solved against --ember-deep, 0.42 gives 2.38 and
   0.56 is the first value that clears. 0.60 measures 3.35:1 and leaves room
   for the gradient. */
.hero-split-copy .btn-outline {
  border-color: rgba(255, 249, 245, 0.6);
  color: var(--on-ember);
}

.hero-split-copy .btn-outline:hover {
  background: rgba(255, 249, 245, 0.11);
  border-color: var(--on-ember);
}

/* --hero-art-ground is coupled to the ASSET, not to taste, and getting this
   backwards produces a visible seam.
   
     transparent cutout  ->  --surface (warm off-white). The panel colour is
                             free, because there is no baked background to
                             match. Warmer reads better and stops the panel
                             feeling like a cold hole.
     opaque packshot     ->  #fff, and only #fff. An ASW packshot is a solid
                             white 2560x2560 canvas, so anything else puts a
                             hard-edged white rectangle on the panel — the
                             same defect that was in the lineup tiles until
                             .plate was flattened to #fff.
   
   Today's asset (hero-air-wide.webp) is RGBA with 42.4% transparent pixels,
   so --surface it is. When a licensed opaque packshot replaces it, flip this
   one value to #fff. */
.hero-split-art {
  --hero-art-ground: var(--surface);

  position: relative;
  display: grid;
  place-items: center;
  min-height: min(50svh, 400px);
  padding: 36px var(--gut);
  background: var(--hero-art-ground);
}

@media (min-width: 900px) {
  .hero-split-art {
    min-height: 420px;
    /* Percentage inline padding so the product tracks the panel. A fixed
       clamp in px does the same thing the shell gutter was doing on the other
       side: it stops scaling while the panel keeps growing. */
    padding: 48px 10%;
  }
}

/* Proportional, with a sanity cap — not a fixed rem width.
   
   40rem looked right at 1512 and collapsed above it, because a 640px cap in a
   panel that grows with the viewport is a shrinking product:
   
       viewport   art panel   product   % of panel
         1512        953        640        67%
         1920       1210        640        53%
         2560       1613        640        40%
         3440       2167        640        30%
   
   The comp runs 79%. Filling the panel's 10% inline padding gives a flat 80%
   from 1280 through about 2560, which is the same relationship at every width.
   The 76rem ceiling exists only so an ultrawide does not draw a 1700px laptop;
   it bites at 3440 and nowhere anyone actually works. */
.hero-split-art .hero-art-img {
  display: block;
  width: min(100%, 76rem);
  height: auto;
  object-fit: contain;
}

/* ---- the band of facts under the diptych -------------------------------- */

/* Painted above the hero, so nothing from the panel can ever sit on its text. */
.hero-band {
  position: relative;
  z-index: 3;
  background: var(--paper);
  border-top: 1px solid var(--hair-light);
}

/* 34px, down from 64px — the 64 was solving a problem that no longer exists.
   
   It was set when the hero was the cream editorial version, where the stack
   had 328px of slack that a headline, two lines, a button and a link did not
   need. Leaving that slack in the hero split it into 164px of void above the
   copy and 164px below, so it was deliberately pushed down here instead,
   where three facts and a photograph could earn the height.
   
   The terracotta hero has no such slack: the panel is a colour field rather
   than type floating in cream, and the copy is anchored to its foot. So the
   band no longer has anything to absorb, and 64px of padding is just a tall
   band — measured, it was making the rail about 60px deeper than the version
   it was modelled on, which had 30px.
   
   34 rather than 30: the shop card's photograph is 188px wide at 3:2, so the
   band's content floor is 125px regardless. 34 keeps the type off that floor
   without inflating the band around it. */
.hero-band-inner {
  display: grid;
  grid-template-columns: 1fr;
  gap: 26px;
  padding-block: 34px;
}

@media (min-width: 1080px) {
  .hero-band-inner {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 40px;
    align-items: center;
  }
}

.hero-band-facts {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

@media (min-width: 620px) {
  .hero-band-facts {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0;
  }

  .hero-band-facts > * {
    padding-right: 24px;
  }

  .hero-band-facts > * + * {
    padding-left: 24px;
    border-left: 1px solid var(--hair-light);
  }
}

.hb-cell {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  min-width: 0;
}

/* The band: the two Apple signatures, each with what it means beside it, in
   two equal columns. 36px signatures: Apple's web figure for a two-line
   signature is 35px.
   The text sits to the right of its logo, centred on it, and wraps on its
   own, balanced so the lines stay even. Whether it sits beside or under the
   logo depends on the band's own width, not the screen's (a container
   query): beside it only where there is room for about three lines. At
   1180px the wider service logo left room for 8 lines. */
.hero-band-mix {
  align-items: center;
}

@media (min-width: 620px) {
  .hero-band-mix {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.hb-cred img {
  display: block;
  flex: 0 0 auto;
  width: auto;
  height: 36px;
  margin-bottom: 12px;
}

.hb-cred > span {
  display: block;
  max-width: 44ch;
  font-size: 12.5px;
  line-height: 1.55;
  text-wrap: balance;
  color: var(--muted-fg);
}

/* Measured on the pair, not each cell, so both switch together (per cell,
   the second one's padding put them on different sides at 1440px). The
   pair is 963px wide at 1024 (shop card below), 668-843px from 1180 to
   1366, 912px+ from 1440. */
.hero-band-facts.hero-band-mix {
  container-type: inline-size;
}

@container (min-width: 880px) {
  .hb-cred-row {
    display: flex;
    align-items: center;
    gap: 20px;
  }

  .hb-cred-row img {
    margin-bottom: 0;
  }
}

/* 24px, up from the 19px the markup asks for. The comp's icons are noticeably
   larger than its label type, which is what stops the cell reading as a line
   of body text with a bullet. Overriding in CSS rather than editing four
   width/height attribute pairs in the template. */
.hb-cell svg {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  margin-top: 1px;
  stroke-width: 1.5;
  color: var(--ink-4);
}

.hb-cell b {
  display: block;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--on-paper);
}

.hb-cell span {
  display: block;
  margin-top: 4px;
  font-size: 12.5px;
  line-height: 1.55;
  text-wrap: pretty;
  color: var(--muted-fg);
}

.hero-band-shop {
  display: grid;
  grid-template-columns: 176px minmax(0, 1fr);
  align-items: center;
  gap: 18px;
  min-width: 0;
}

.hbs-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
}

@media (min-width: 1080px) {
  .hero-band-shop {
    max-width: 27rem;
    padding-left: 40px;
    border-left: 1px solid var(--hair-light);
  }
}

/* height: auto is load-bearing, not boilerplate.
   The reset in this sheet is `img, svg { display: block; max-width: 100% }`
   with no height rule, so an <img> carrying width/height attributes keeps
   height as a presentational hint unless CSS overrides it — and aspect-ratio
   cannot override an explicit height. Without this line the storefront came
   out 112x720 (its attribute height), which inflated the whole band to 769px
   and left the facts sitting in a 700px void. Every width in this file is
   paired with height: auto for exactly this reason; see .hero-caption-shot. */
.hero-band-shop img {
  display: block;
  width: 176px;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--r-sm);
  filter: var(--grade-real-sun);
  box-shadow: 0 0 0 1px rgba(20, 17, 14, 0.08), var(--e2);
}

.hero-band-shop > picture {
  display: block;
  width: 176px;
}

/* Live store hours. --ember-deep when open rather than --ember, because at
   12.5px --ember measures 3.94:1 on --paper and this is body-size text.
   deviceland.js adds .is-shut when the shop is closed, which drops it back to
   the muted grey — an "open now" in brand colour that stays coloured at
   11pm is worse than no signal. */
.hbs-open {
  margin-top: 6px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ember-deep);
}

.hbs-open.is-shut,
.hbs-open .is-shut {
  font-weight: 500;
  color: var(--muted-fg);
}

.hbs-dir {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 9px;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--on-paper);
  transition: color var(--dur-ui) var(--ease);
}

.hbs-dir span {
  border-bottom: 1px solid var(--hair-light-2);
  padding-bottom: 1px;
}

.hbs-dir svg {
  transition: transform var(--dur-ui) var(--ease);
}

.hbs-dir:hover {
  color: var(--ember-deep);
}

.hbs-dir:hover span {
  border-bottom-color: currentColor;
}

.hbs-dir:hover svg {
  transform: translateX(3px);
}

.hbs-key {
  display: block;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ember-deep);
}

.hbs-txt {
  display: block;
  margin-top: 6px;
  font-size: 12.5px;
  line-height: 1.5;
  text-wrap: pretty;
  color: var(--ink-4);
}

/* ------------------------------------------------------------
   Photography layer — replaces vector fallbacks on load
   ------------------------------------------------------------ */

/* Serving AVIF with a WebP fallback needs a <picture> wrapper, where the
   reference used a bare <img>. display:contents was tried first, to make the
   wrapper's box vanish so every existing measurement still held. It cannot be
   used: in Safari and Chromium, display:contents on a <picture> inside an
   <a> drops the image out of the link's hit-testing tree, so clicks on the
   photograph — most of the tile — do nothing, and only the type at the
   bottom of the card still navigates.

   The wrapper is therefore a real box, sized to fill the same rectangle the
   bare <img> used to occupy. The img rules below then fill the wrapper. */
.hero-media > picture,
.svc-media > picture {
  display: block;
  width: 100%;
  height: 100%;
}

/* .prod-media is no longer in this list. The floor cards hold a
   .packshot-well now, and a well centres its image in flow inside a 12% mat —
   absolutely positioning it to inset:0 would stretch it back to the edges and
   undo the mat. .tile keeps the treatment: its photography is ours, so
   full-bleed is still allowed there. */
.tile > picture {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
}

.owner-figure > picture,
.visit-shot > picture,
.spec-card figure > picture {
  display: block;
  width: 100%;
}

.hero-caption > picture {
  display: block;
  width: 148px;
  margin: 0 0 14px auto;
}

.hero-media.has-photo .tableau,
.tile.has-photo .tile-stage,
.prod-media.has-photo .dev,
.svc-media.has-photo .bench,
.logo.has-photo svg {
  display: none;
}

/* Scroll-linked, not entrance-triggered: --par is written by rAF from the page
   offset, so the frame drifts against the copy instead of animating once. */
/* object-position is doing the work a stacked layout would otherwise have to.

   Narrow viewports put the copy directly over the frame, and the frame's busiest
   region is the open machine's keyboard — legible (5.1:1 at worst) but visually
   noisy under type. Panning the frame down and right on phones puts the closed
   lid and the empty cove behind the headline and the spec row instead, and
   leaves the keyboard below the fold of the copy. Same photograph, same
   full-bleed effect, no collision. */
.hero-photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 74% 76%;
  user-select: none;
  transform: scale(1.04) translate3d(0, var(--par, 0px), 0);
  will-change: transform;
}

@media (min-width: 640px) {
  .hero-photo {
    object-position: 68% 60%;
  }
}

@media (min-width: 1024px) {
  .hero-photo {
    object-position: 88% 52%;
  }
}

/* The hero stays full-bleed at every width, including phones.

   A stacked variant was tried below 768px and reverted. The reasoning for it was
   sound in the abstract — a portrait viewport crops a landscape frame hard, so
   the copy sits over busy keyboard detail — but it traded the hero's whole
   effect for a problem that measurement says does not exist: overlaid type runs
   5.1:1 to 17.3:1 against the composited photograph and passes AA everywhere.

   Pushing the image below the copy also pushed it below the fold, so the first
   screen became type on blank paper. On the device carrying most of the traffic
   that is the worst possible trade: guaranteed legibility nobody asked for, in
   exchange for the one image that makes the page feel like a shop rather than a
   template.

   What actually needed tuning was where the frame sits, not whether it is
   there. See object-position below. */
@media (max-width: 767px) {
  /* Bottom-aligned, not centred — and this is the whole fix for short phones.

     The portrait asset is aspect 0.4615, which matches iPhone 12-16 exactly. But
     an iPhone SE is 0.562, so `cover` has to discard 151px of height, and a
     CENTRED crop takes half of that off the bottom — precisely where the product
     is. Measured on SE 2/3 the machine had 80px of visible height against 163px
     on a 14.

     Anchoring to the bottom spends the entire crop on the cove at the top, which
     is the one part of the frame that exists to be spent: it is extended
     backdrop, and the copy sitting on it does not care how much there is. */
  .hero-photo {
    object-position: 50% 100%;
  }

  /* Dropped on phones. Measured, the product band lands at y=561 on an 844px
     screen and the spec row occupied 590-679 — directly across the machine.
     Every one of its three facts already appears within one screen of it:
     "Walk-in / No appointment" is the second rail item verbatim, 1985 is in the
     rail's Premier Partner line and again in Receipts, and free parking is in
     the shop caption and the Visit block. It was the one element in the hero
     that could be removed without removing information. */
  .spec-row {
    display: none;
  }

  /* ---- The hero geometry below is now width-gated, not height-gated. ---------

     It used to live in a `(max-height: 740px)` block, which is why an iPhone 12
     Pro at 390x844 still showed the machine chopped: 844 > 740, so none of it
     applied. There the rail stayed INSIDE the 100svh hero at y 724-844, opaque,
     directly across the machine — the frame ran the full height of the section
     underneath it.

     Nothing about this is specific to short screens. Every phone wants the same
     three things, so every phone now gets them:

       1  the frame stops at the fold
       2  the copy owns the first screen, which puts the rail below it
       3  the rail sits on paper, so it needs no band of its own

     That is the generic solution: one rule set for all phone widths, with the
     height queries left to do only what they are actually about — reclaiming
     vertical space for type on short screens. */
  .hero-grid {
    align-content: start;
    padding-block: calc(var(--head-h) + 14px) 0;
    min-height: 100svh;
  }

  .hero {
    min-height: 0;
  }

  /* .hero-media is inset:0 of .hero, and .hero is now taller than the viewport
     because the rail follows the full-height grid. Capping the frame at one
     screen is what keeps it off the rail. */
  .hero-media {
    height: 100svh;
    bottom: auto;
  }

  .rail-inner {
    padding-block: 18px;
    gap: 12px;
  }

  /* The veil must reach ZERO before the product begins, not merely thin out.

     The base rule is built for the LANDSCAPE frame, where type sits over the
     keyboard and the bottom has to be covered for the rail — it holds 0.72 at
     34% and rises again to 0.86 at 100%. Applied to the portrait frame, where
     the machine occupies roughly 65%-95% of the height, that bottom ramp lays a
     milky wash straight over the product. Hiding this single element restored a
     deep teal screen, a dark bezel and a strong cast shadow, none of which were
     visible as shipped.

     Measured: the scrim was lifting the machine's rows by +75 to +140 on an SE.

     Zero is reached at 70%, which clears the product on every viewport measured
     while still covering the copy — the last button ends between 52% and 66%
     depending on the device, and 0.7 is still in force at 55%.

     This lives in the WIDTH-gated block deliberately. A first attempt put it in
     the (min-height: 780px) block by accident, which fixed a 14 Pro Max and left
     every short phone exactly as washed out as before — the same brace-placement
     error that has now bitten this file three times. */
  .hero-scrims {
    background: linear-gradient(
        to bottom,
        rgba(248, 242, 232, 0.9) 0%,
        rgba(248, 242, 232, 0.84) 38%,
        rgba(248, 242, 232, 0.7) 55%,
        rgba(248, 242, 232, 0.18) 64%,
        rgba(248, 242, 232, 0) 70%
      ),
      radial-gradient(
        70% 34% at 50% 20%,
        rgba(201, 89, 40, 0.1),
        transparent 72%
      );
  }
}

/* Reverted: this block used to push the copy down 58px on tall phones to absorb
   surplus height. It traded one awkwardness for a worse one — a visible void
   between the masthead and the status pill, which reads as a mistake rather than
   as spacing, while the gap it was closing read merely as generous.

   Surplus height is now handled where it belongs, in the asset: the product is
   placed against a measured buffer at the bottom of the frame instead of being
   flush against its edge, so the composition has somewhere to breathe without
   the copy moving at all. See mobilehero.mjs. */
@media (max-width: 767px) and (min-height: 780px) {
  .hero-grid {
    padding-block: calc(var(--head-h) + 18px) 0;
  }

  /* Surplus height is spent on the copy's INTERNAL rhythm, not on a single block
     of padding above it.

     A tall phone leaves ~140px unused between the last button and the machine.
     The previous attempt absorbed it by pushing the whole copy down 58px, which
     put the surplus in one place — between the masthead and the status pill —
     where it read as a void rather than as spacing.

     Distributed across three existing gaps it reads as generous typography,
     which is what a large screen should look like, and it closes the gap below
     without moving the top of the copy at all. */
  /* Bigger type, not just bigger gaps. A 932px screen showing the same 50px
     headline as a 667px one is the actual reason it felt empty — the composition
     was not using the screen it was given, it was only spacing out on it. */
  .hero-copy .h-xl {
    font-size: clamp(3.1rem, 15vw, 3.9rem);
    margin-top: 44px !important;
  }

  .hero-copy .lede {
    margin-top: 34px;
    font-size: 16px;
    line-height: 1.62;
  }

  .hero-cta {
    margin-top: 44px;
  }

  /* The rail still crosses the lower part of the frame, so it stops being a
     transparent overlay and becomes an opaque band — which is what it already
     looks like on desktop, where the photograph has faded out beneath it. */
  /* Transparent: the rail now sits on the section's own paper below the fold,
     not over the photograph, so it needs no band of its own. */
  .rail {
    background: transparent;
    box-shadow: none;
  }

}

/* Short phones. iPhone SE 2/3 is 375x667 and SE 1 is 320x568 — both still in
   real use, and neither was ever tested here; every mobile number quoted in this
   file until now came from a 390x844 iPhone 14.

   667px has to hold a 96px masthead, the full hero copy, a 120px assurance rail
   and a 57px call bar. That is 273px of chrome before a single word, so the
   product ends up with 80px and the rail's fourth item is sliced by the call
   bar. Height has to be reclaimed rather than redistributed. */
@media (max-width: 767px) and (max-height: 740px) {
  /* 96 -> 76. The lockup keeps its slogan; only the padding around it goes.

     --head-h-stuck is set to match, not left at its 78px default. Overriding only
     --head-h meant the condensed header was 2px TALLER than the resting one, so
     scrolling made the bar grow while a height transition ran and the logo shrank
     inside it. */
  :root {
    --head-h: 76px;
    --head-h-stuck: 76px;
  }

  /* min-height and align-content are set in the width-gated block above and
     apply to every phone; only the top padding tightens here. */
  .hero-grid {
    padding-block: calc(var(--head-h) + 10px) 0;
  }

  /* Two lines at 120px is 170px of headline on a 667px screen. */
  .hero-copy .h-xl {
    font-size: clamp(2.35rem, 13.5vw, 3.2rem);
    margin-top: 14px !important;
  }

  .hero-copy .lede {
    margin-top: 12px;
    font-size: 14px;
    line-height: 1.48;
  }

  /* Every value below is reclaiming vertical space for one reason: the machine is
     bottom-anchored and therefore sits a FIXED 239px above the bottom edge of the
     screen at 415px wide, while the copy is pinned to the top. Shorter viewports
     close the gap between them from below, so the copy has to give it back.

     Measured overlap between the phone button and the machine's ink before this:
     11px at 375x667, 59px at 415x646. */
  .hero-cta {
    margin-top: 16px;
    gap: 10px;
  }

  .hero-cta .btn {
    padding-block: 13px;
  }

  .hero-copy .pill {
    padding-block: 6px;
  }

  /* .hero-media capping, .hero-grid min-height and the transparent rail used to
     live here. They moved up into the width-gated block, because none of them
     were ever about screen HEIGHT — height-gating them is exactly why an iPhone
     12 Pro at 390x844 still had the rail sitting across the machine.

     What legitimately belongs in a height query is only this: on a short screen
     the rail's own padding is worth trimming. */
  .rail-inner {
    padding-block: 14px;
    gap: 9px;
  }
}

/* Viewports under 660px tall. Safari reports the SMALL viewport to `svh`, so this
   is what an SE 1 becomes with the chrome expanded — it is a real state, not a
   contrived one.

   At 546px the copy needs 379px and the machine's ink begins at 319px, so no
   amount of tightening closes a 60px deficit. One block has to go, and the lede
   is the only candidate: the status pill answers "are they open", the headline
   carries the proposition, and the two buttons are the actions. The lede is
   supporting prose, which is what a print designer cuts first.

   It is hidden visually rather than removed, so a screen reader still announces
   it and no information is actually lost — the same reason .sr-only exists
   elsewhere in this file. */
@media (max-width: 767px) and (max-height: 660px) {
  .hero-copy .lede {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .hero-copy .h-xl {
    font-size: clamp(2.1rem, 12.5vw, 2.8rem);
  }
}

.svc-photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The picks row is deliberately colour-varied (orange, lavender, cream, orange).
   sepia-shifting one card to "fix" that was reading intentional variety as an
   error — as well as grading merchandise colour, which the rules forbid. */
/* .prod-photo is gone, and the note above is why it is worth reading twice.
   It correctly identified that grading merchandise colour is forbidden, then
   left three other breaches of the same rule set in place on the same
   element: object-fit:cover cropped the photo, the hover scaled it 1.07, and
   the "deliberate colour variety" being defended was four renders produced
   here rather than Apple assets at all. The colour was not variety, it was
   four different invented studio grounds.
   The floor cards now use .packshot-well / .packshot-img. See the packshot
   block above for the boundary those two classes enforce. */

/* ------------------------------------------------------------
   Device stage — SVG product artwork
   ------------------------------------------------------------ */

.sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* Vector fallback, staged where the photograph's subject sits so the
   composition survives even when no photography is present. */
.tableau {
  position: absolute;
  right: 0;
  bottom: 12%;
  width: 96%;
  aspect-ratio: 6 / 5;
}

@media (min-width: 1024px) {
  .tableau {
    right: 1%;
    bottom: 14%;
    width: min(58%, 860px);
    aspect-ratio: 7 / 5;
  }
}

.dev {
  position: absolute;
  display: block;
  height: auto;
}

.dev-mac {
  left: 3%;
  top: 8%;
  width: 74%;
  filter: drop-shadow(0 26px 26px rgba(20, 17, 14, 0.3))
    drop-shadow(0 64px 70px rgba(20, 17, 14, 0.24));
  animation: floatA 14s var(--ease-io) infinite;
}

.dev-phone {
  right: 1%;
  top: 26%;
  width: 20%;
  filter: drop-shadow(0 20px 22px rgba(20, 17, 14, 0.32))
    drop-shadow(0 48px 54px rgba(20, 17, 14, 0.22));
  animation: floatB 11s var(--ease-io) infinite;
}

.dev-watch {
  left: 1%;
  bottom: 2%;
  width: 17%;
  filter: drop-shadow(0 16px 20px rgba(20, 17, 14, 0.34))
    drop-shadow(0 40px 44px rgba(20, 17, 14, 0.2));
  animation: floatC 12.5s var(--ease-io) infinite;
}

.dev-pods {
  left: 34%;
  bottom: 0%;
  width: 21%;
  filter: drop-shadow(0 16px 20px rgba(20, 17, 14, 0.3))
    drop-shadow(0 40px 46px rgba(20, 17, 14, 0.18));
  animation: floatD 13.5s var(--ease-io) infinite;
}

@keyframes floatA {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-12px);
  }
}

@keyframes floatB {
  0%,
  100% {
    transform: translateY(0) rotate(4deg);
  }
  50% {
    transform: translateY(-10px) rotate(4deg);
  }
}

@keyframes floatC {
  0%,
  100% {
    transform: translateY(0) rotate(-6deg);
  }
  50% {
    transform: translateY(-8px) rotate(-6deg);
  }
}

@keyframes floatD {
  0%,
  100% {
    transform: translateY(0) rotate(2deg);
  }
  50% {
    transform: translateY(-7px) rotate(2deg);
  }
}

/* Device inside a bento tile — vector fallback, in the same band as the photo */
.tile-stage {
  position: absolute;
  inset: 7% 4% 34%;
  pointer-events: none;
}

.tile-stage .dev {
  position: absolute;
  inset: 0;
  margin: auto;
  width: auto;
  height: 94%;
  max-width: 100%;
  transform: none;
  animation: none;
}

.dev-tile-iphone {
  right: -10%;
  top: 6%;
  width: 62%;
  transform: rotate(6deg);
  filter: drop-shadow(0 22px 26px rgba(20, 17, 14, 0.24))
    drop-shadow(0 54px 60px rgba(20, 17, 14, 0.16));
  transition: transform var(--dur-slow) var(--ease);
}

.dev-tile-iphone-2 {
  right: 26%;
  top: 16%;
  width: 52%;
  transform: rotate(-7deg);
  filter: drop-shadow(0 18px 22px rgba(20, 17, 14, 0.22))
    drop-shadow(0 44px 50px rgba(20, 17, 14, 0.14));
  transition: transform var(--dur-slow) var(--ease);
}

.dev-tile-ipad {
  right: -8%;
  top: 4%;
  width: 60%;
  transform: rotate(-8deg);
  filter: drop-shadow(0 20px 24px rgba(20, 17, 14, 0.22))
    drop-shadow(0 48px 54px rgba(20, 17, 14, 0.15));
  transition: transform var(--dur-slow) var(--ease);
}

.dev-tile-watch {
  right: 4%;
  top: 6%;
  width: 42%;
  transform: rotate(7deg);
  filter: drop-shadow(0 18px 22px rgba(20, 17, 14, 0.26))
    drop-shadow(0 42px 48px rgba(20, 17, 14, 0.16));
  transition: transform var(--dur-slow) var(--ease);
}

.dev-tile-pods {
  right: 2%;
  top: 8%;
  width: 52%;
  transform: rotate(-5deg);
  filter: drop-shadow(0 18px 22px rgba(20, 17, 14, 0.2))
    drop-shadow(0 42px 48px rgba(20, 17, 14, 0.14));
  transition: transform var(--dur-slow) var(--ease);
}

.tile:hover .tile-stage .dev {
  transform: translateY(-5px) scale(1.05);
}

.dev-tile-iphone {
  --tilt: 6deg;
}
.dev-tile-iphone-2 {
  --tilt: -7deg;
}
.dev-tile-ipad {
  --tilt: -8deg;
}
.dev-tile-watch {
  --tilt: 7deg;
}
.dev-tile-pods {
  --tilt: -5deg;
}

@media (max-width: 759px) {
  .dev-tile-iphone {
    right: -4%;
    width: 46%;
  }
  .dev-tile-iphone-2 {
    right: 28%;
    width: 38%;
  }
  .dev-tile-ipad,
  .dev-tile-pods {
    width: 44%;
  }
  .dev-tile-watch {
    width: 32%;
  }
}

/* ------------------------------------------------------------
   Section headers
   ------------------------------------------------------------ */

/* mb-12 flex flex-wrap items-end justify-between gap-6 */
.sec-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: 48px;
}

/* mb-3 text-[11px] uppercase tracking-[0.32em] text-muted-foreground */
.sec-head .eyebrow {
  margin-bottom: 12px;
  letter-spacing: 0.32em;
}

/* ------------------------------------------------------------
   Lineup — bento
   ------------------------------------------------------------ */

/* Ported from the reference's own markup, not reconstructed from a
   screenshot. Tailwind source in brackets so the next person can diff it.

     grid grid-cols-1 gap-4 md:grid-cols-6 md:grid-rows-2 md:gap-5 lg:h-[820px]

   Two fixed rows inside a fixed 820px height, not auto-rows: the tall tile is
   exactly the two short rows plus the gap, which is what makes the column
   edges line up. */
.bento {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

/* FOUR columns, not six.
   ------------------------------------------------------------
   The packshots are real catalogue images at 2560x2560 — perfectly square —
   drawn with object-fit:contain, so each one renders at min(boxWidth,
   boxHeight). That single fact decides the whole grid:

     tile shape           photo box     product   dead space
     2 of 6 x 2 rows        413x658       413       245px
     2 of 4 x 2 rows        670x658       658         0px

   In a six-column grid the tall tile was 453px wide, so its product was
   WIDTH-limited at 413px and the other 245px of height could not be used by
   anything — it became letterbox. The product was already as large as that
   shape allowed; it looked small because of the void around it, which is why
   shrinking or moving the type would not have gained a pixel.

   Two of four columns makes the tall tile 690px wide, and the product jumps to
   658px: +59%, with the dead space gone. Its photo box is now very nearly
   square, which is exactly what a square asset wants.

   The single-column tiles go from 453px to 335px wide, and lose nothing: they
   are HEIGHT-limited (box 295x238, so 238px either way), because their photo
   row gets whatever the label and the copy leave. For those, and only those,
   the type genuinely is the constraint. */
@media (min-width: 760px) {
  .bento {
    grid-template-columns: repeat(4, 1fr);
    grid-template-rows: repeat(2, 1fr);
    gap: 20px;
    height: 700px;
  }
  .tile-tall {
    grid-column: span 2;
    grid-row: span 2;
  }
  .tile-wide {
    grid-column: span 1;
  }
  /* Set by deviceland_bento_tiles() when a category drops out, so the 2x2
     block beside the tall tile never shows a hole. */
  .tile-wide.tile-span-2 {
    grid-column: span 2;
  }
}

@media (min-width: 1024px) {
  .bento {
    height: 820px;
  }
}

/* rounded-3xl = 24px. bg-surface. No hairline and no elevation: the
   photograph is the tile, so an outline would draw a box around a picture. */
/* A three-row grid, not a flex column with an absolutely positioned photo.
   
   The old arrangement reserved a fixed foot (--tile-foot) for the type and
   sized the photo's padding-bottom to match. That cannot work, because the
   type's height is not a constant: .tile-copy is two title lines at 34.6px
   plus a price row plus 56px of padding, which measures 158px against the
   104px that was reserved — so it overran the photograph by 54px. On the tall
   tile it was 244px against 138px, a 106px overrun. That is the overlapping
   text, and no amount of tuning the reserved number fixes it, because the
   number depends on how long the headline happens to be.
   
   Three rows — label, photograph, copy — with the middle row at
   minmax(0, 1fr) means the photograph gets exactly what the other two leave
   and can never be overlapped by either, at any headline length. */
.tile {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr) auto;
  border-radius: var(--r-tile);
  overflow: hidden;
  background: #fff;
  color: var(--on-paper);
  isolation: isolate;
  transition: transform var(--dur-reveal) var(--ease);
}

.tile:hover {
  transform: translateY(-4px);
}

/* Vector fallback ground. Only visible if the photograph fails to load. */
/* Vector fallback ground. Invisible under a full-bleed photograph, which is
   exactly why it can be a strong warm gradient. */
/* Flat #fff, and the gradient that was here had to go.
   
   It ran #fff to --bone to --surface down the tile, which looked right in
   isolation and produced a visible hard-edged rectangle once a real packshot
   was in front of it: the asset is a pure #ffffff canvas, so wherever the
   plate had warmed past white the asset's own square edge showed as a seam.
   The same white-rectangle problem that made the earlier plate heroes read as
   holes, relocated inside the tiles.
   
   So the two layers get clean roles instead of both trying to shade:
   
     .plate       the white ground the asset sits on. Matches #ffffff exactly,
                  so the asset's edge is invisible and tile and image read as
                  one surface. Also keeps us inside "a white, off-white, light
                  gray, or black background" without argument.
     .tile-scrim  the shaded foot the headline sits on, bottom-anchored to
                  --tile-foot so it is entirely below the photograph.
   
   All the shading the tiles had is still there; it is just in the foot, where
   there is no product photo for it to sit on top of. */
.plate {
  position: absolute;
  inset: 0;
  z-index: -2;
  background: #fff;
}

/* Full-bleed object-cover, which is the reference's own treatment: the
   photograph IS the tile. A cut-out centred on a CSS plate was tried earlier in
   this project and rejected — it is why nothing lined up — and reintroducing it
   was a mistake. It only looked necessary because three of the nine product
   images were defective; the fix for a bad photograph is a better photograph,
   not a different layout system. */
/* contain, and inset so the type has clean ground.
   The note above is right that a cut-out on a plate was rejected once — but
   that was with the synthesised renders, which had their own baked
   backgrounds and so had to bleed. A real packshot arrives as a product
   centred on flat white with its own margin, and `cover` on a 1:1 asset in a
   2:1 tile throws away half of it. So: contained, pushed up into the top
   two-thirds by object-position, with the bottom third left to .tile-scrim
   and the headline. The tile's own ground is white, matching the asset's, so
   the seam between image and plate is invisible and the two read as one lit
   surface.

   No transform on hover. It was scale(1.04) on the photograph, which is an
   Apple product photo — "do not animate or enhance". .tile:hover already
   lifts the whole card by 4px, so the affordance is unchanged. */
/* Pixel padding, and both of the obvious alternatives are wrong.
   
   Percentage padding resolves against the WIDTH on all four sides, so
   `padding: 9% 9% 30%` put a 137px foot on a 390px-tall tile, squeezed the
   contained asset into a 212px box and rendered the product at about 127px —
   a thumbnail.
   
   Sizing by inset instead does not work at all on an <img>. It is a replaced
   element, and per CSS 2.1 10.3.8 an absolutely positioned replaced element
   with `width: auto` takes its INTRINSIC width and then ignores `right`. So
   `inset: 5% 5% 27% 5%` sized each image to 2560px, and with the tile's
   overflow hidden every tile showed the top-left corner of the asset — which
   on a packshot is its blank white margin. The products disappeared entirely.
   
   So: inset 0, fill the tile, and mat with padding in px, which resolves the
   same on both axes. The foot is what .tile-scrim and the headline occupy.
   The sides are shallow because the packshot already carries roughly 15% of
   its own margin. */
/* --tile-foot is gone. It was a hand-reserved band for the type, and the
   grid rows above now derive the same division from the type's real height,
   which is what it was standing in for. */

/* A grid item in row 2 rather than an absolutely positioned overlay. It fills
   its own row, so the padding is optical breathing room and not a
   reservation for anything else — the rows handle the division now. */
/* --trim scales the packshot past the white canvas inside the vendor file.
   ------------------------------------------------------------
   Measured, the catalogue images are mostly margin: the iPhone's product fills
   52% of its 2560px width, with 608px of pure white each side. So a tile can
   be laid out perfectly and the product still reads small, because a third of
   the frame is empty before CSS is involved.

   Apple's rules forbid omitting portions of a product photograph, so the file
   is not cropped. This scales the rendered image instead and lets the tile's
   existing overflow:hidden clip white only — the asset on disk is untouched,
   the whole product stays visible, and it is one custom property to undo.

   The factor comes from deviceland_packshot_trim(), which measures each
   attachment once and caches it, so it holds for any future featured product
   rather than being four numbers that rot. It is capped so the product's
   larger side reaches 88% of the rendered square, which cannot clip the
   product at any tile shape and leaves the mat that "do not crowd Apple
   product images" asks for.

   transform, not width: it does not disturb the three-row grid the photo sits
   in, and it is not an animation or an enhancement of the photograph — the
   image is drawn at one fixed size, exactly as a larger <img> would be. */
/* Clipped to its own row (Oct 14). The scale above makes the photo PAINT
   past its grid row: about 30px into the title row on a 400px tile, in every
   browser. Chrome happened to draw the title on top. Safari puts transformed
   elements on their own compositing layer and can draw them over later text,
   so the photo covered "Apple Watch." and would cover the shading too. The
   clip undoes exactly the scale at the edges: inset by 50% - 50%/trim in the
   image's own coordinates, so after scaling the visible area is precisely the
   row the image was laid out in. Only white canvas is clipped (the trim keeps
   the product within 88% of the drawn square), never the product. */
.tile-photo {
  grid-area: 2 / 1;
  z-index: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  transform: scale(var(--trim, 1));
  clip-path: inset(calc(50% - 50% / var(--trim, 1)));
  padding: 2px 20px 12px;
  object-fit: contain;
}

/* Shallow sides on the tall tile: a square asset in a tall row is
   width-limited, so the side padding is what decides how large the product
   draws. 10px takes it from 375px to about 435px. */
.tile-tall .tile-photo {
  padding: 2px 10px 14px;
}

/* bg-gradient-to-t from-surface from-30% via-surface/80 via-55%
   to-transparent to-85%

   The photograph runs edge to edge and the bottom third is faded back to the
   tile colour, so the headline sits on solid ground with no box, no panel and
   no dead space between product and type. */
/* Bottom-anchored, and this one was a real exposure rather than a nicety.
   At inset:0 the gradient reached transparency only at 85% of the tile
   height, which means measured from the top it began washing at 15% and ran
   opaque to the foot. On a 390px tile the product's content box was y 22-286
   and the scrim covered y 58-286 of it: a translucent graphic laid over 228px
   of an Apple product photograph. That is squarely inside "do not alter Apple
   product photos or add type, violators, or other graphics", and it is the
   kind of thing a field rep would spot in a screenshot.

   Now it is simply a grid item sharing row 3 with .tile-copy, so it stretches
   to whatever height the type actually is — no reserved number, and still
   guaranteed to sit entirely below the photograph, which occupies row 2. It
   shades OUR ground and never touches the asset. */
/* Restored (owner, Oct 14). It was removed on Oct 11 when the tiles looked
   wrong in Safari, but the cause was the photo painting into this row (see
   .tile-photo's clip-path), not the shading. It shades our ground in row 3
   only, behind the type, and never touches the photograph. */
.tile-scrim {
  grid-area: 3 / 1;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    to top,
    var(--surface) 0%,
    var(--surface) 58%,
    transparent 100%
  );
}

/* Tighter than the old flat 28px. The rows now compete for the same tile
   height, so every pixel of chrome here is a pixel off the product: dropping
   the inner edges to 10px gives the photograph roughly 60px back on a wide
   tile without the type feeling cramped against it. */
.tile-head {
  grid-area: 1 / 1;
  position: relative;
  z-index: 2;
  padding: 24px 24px 10px;
}

.tile-copy {
  grid-area: 3 / 1;
  position: relative;
  z-index: 2;
  padding: 10px 24px 24px;
}

@media (max-width: 759px) {
  /* One column, content-sized rows: give the photograph a stage to fill or
     the tile collapses to the height of its own text. */
  .tile {
    min-height: 460px;
  }
}

/* text-[11px] uppercase tracking-[0.28em] text-muted-foreground */
/* 0.28em tracking at 11px separates the digits of "16" far enough that it reads
   as "18" — tracking is applied per character, so numerals suffer worse than
   letters, which have internal counters to hold them together. Lining tabular
   figures and slightly tighter tracking fix it without losing the small-caps
   feel. Model numbers are the one place on a product page you cannot afford an
   ambiguous glyph. */
.tile-label {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2em;
  font-variant-numeric: lining-nums tabular-nums;
  text-transform: uppercase;
  color: var(--muted-fg);
}

/* font-display tracking-[-0.02em] leading-[1] text-balance
   tall  text-[clamp(2.4rem,3.6vw,4.4rem)]   -> 51.84px at 1440
   small text-[clamp(1.8rem,2.4vw,2.8rem)]   -> 34.56px at 1440 */
.tile-title {
  font-family: var(--display);
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1;
  text-wrap: balance;
  font-size: clamp(1.8rem, 2.4vw, 2.8rem);
}

/* The single-column tiles need a smaller line, and this is not a taste call.
   Those tiles are HEIGHT-limited: their photo row takes whatever the label and
   the copy leave, so every pixel the title grows is a pixel the packshot
   loses. Moving the grid to four columns narrowed them from 453px to 335px and
   "From iPad to iPad Pro." wrapped to two lines — the title went 36px to 73px
   and each photo lost 37px, straight out of the product.

   At 335px wide a 54.7px display line cannot hold these titles on one line;
   about 32px can. So the small tiles get their own size and hand the height
   back to the photograph, which is the thing being sold.

   .tile-tall keeps the large line: it is width-limited rather than
   height-limited, so its title costs the product nothing. */
.tile:not(.tile-tall):not(.tile-ink) .tile-title {
  font-size: clamp(1.4rem, 1.9vw, 2rem);
}

.tile-tall .tile-title {
  font-size: clamp(2.4rem, 3.6vw, 4.4rem);
}

/* mt-5 flex items-center justify-between text-[13px] */
.tile-foot {
  margin-top: 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  font-size: 13px;
}

.tile-price {
  color: var(--muted-fg);
}

/* h-9 w-9 rounded-full bg-ink text-ink-foreground,
   group-hover:bg-primary group-hover:text-primary-foreground */
.chip {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  flex-shrink: 0;
  border-radius: var(--r-pill);
  background: var(--ink);
  color: var(--on-ink);
  transition: background var(--dur-ui) var(--ease),
    color var(--dur-ui) var(--ease);
}

.tile:hover .chip {
  background: var(--ember);
  color: var(--on-ember);
}

/* The one departure from the reference. It specifies bg-ink here; a black
   panel is out, so the grid's non-product exception takes the brand's own
   colour instead. Everything else about the tile is unchanged. */
.tile-ink {
  color: var(--on-ember);
}

/* Same depth treatment as the hero's copy panel: highlight, diagonal
   darkening, inset vignette, film grain. The first two were already here; the
   vignette is the layer that makes a saturated field read as lit rather than
   as a swatch, and it is the one the reference build has that we did not. */
.tile-ink .plate {
  z-index: 0;
  box-shadow: inset 0 0 90px rgba(38, 12, 0, 0.4);
  background: radial-gradient(
      108% 82% at 78% 4%,
      rgba(255, 255, 255, 0.22) 0%,
      transparent 60%
    ),
    linear-gradient(
      154deg,
      var(--ember-deep) 0%,
      var(--ember-deep) 48%,
      var(--ember-deep-2) 100%
    );
}

/* 0.86 measured 2.62:1 — below even the 3:1 floor that non-text UI has to
   clear, let alone text. Opacity alone was not enough: at 11px with tracking,
   weight 400 strokes are thin enough that antialiasing dominates and the glyph
   never reaches its nominal colour. Weight is doing as much work here as
   colour is. */
.tile-ink .tile-label {
  font-weight: 600;
  color: rgb(255, 252, 250);
}

.tile-ink .tile-title {
  font-size: clamp(2rem, 3.4vw, 3.4rem);
}

.trade-list {
  margin: 22px 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* These are <li> labels inside the tile's single anchor, not buttons — they
   answer "what do you take?", which is the question a tile with no product
   photograph raises. Sized deliberately below the page's real controls (32px
   against 35.5px nav links and 38px icon buttons) so they do not promise a
   per-device action that does not exist.

   Enlarged and set to full white because 12px at 92% measured 3.27:1 on this
   ember, under the 4.5:1 AA needs. See the note on --ember-text-floor. */
.trade-list li {
  padding: 7px 14px;
  border: 1px solid rgba(255, 249, 245, 0.44);
  border-radius: var(--r-pill);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: rgb(255, 252, 250);
}

.tile-ink .tile-foot {
  margin-top: 16px;
}

/* Was the worst text on the page at 2.53:1. Dimming body copy to 60% is a habit
   that works on paper and quietly breaks on a mid-tone brand colour. */
.tile-ink .tile-price {
  color: rgba(255, 249, 245, 0.93);
}

.tile-ink .chip {
  background: rgba(255, 255, 255, 0.1);
  color: var(--on-ember);
}

.tile-ink:hover .chip {
  background: var(--on-ember);
  color: var(--ember-field-2);
}

/* ------------------------------------------------------------
   Assurance
   ------------------------------------------------------------ */

/* No rules between cells. Full-height dividers turn four assurances into a
   spec table; icon, heading and one line is enough to separate them. */
.assure {
  display: grid;
  grid-template-columns: 1fr;
  gap: 4px 40px;
  border-top: 1px solid var(--hair-light);
  border-bottom: 1px solid var(--hair-light);
}

@media (min-width: 680px) {
  .assure {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1024px) {
  .assure {
    grid-template-columns: repeat(4, 1fr);
  }
}

.assure-cell {
  padding: 34px 0 38px;
}

.assure-cell svg {
  color: var(--ember);
  margin-bottom: 18px;
}

.assure-cell h3 {
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.42rem;
  letter-spacing: -0.022em;
  line-height: 1.1;
}

.assure-cell p {
  margin-top: 9px;
  font-size: 13.5px;
  line-height: 1.55;
  color: rgba(22, 19, 15, 0.62);
}

/* ------------------------------------------------------------
   Service
   ------------------------------------------------------------ */

.svc-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 48px;
  align-items: start;
}

@media (min-width: 1024px) {
  .svc-grid {
    grid-template-columns: 5fr 7fr;
    gap: 68px;
  }
}

.stat-row {
  margin-top: 54px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
  border-top: 1px solid var(--hair-dark);
  padding-top: 28px;
}

.stat-val {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.6rem, 2.2vw, 2.1rem);
  letter-spacing: -0.02em;
  line-height: 1;
}

.stat-key {
  margin-top: 9px;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: rgba(42, 32, 24, 0.58);
}

.svc-frame {
  position: relative;
}

.svc-media {
  position: relative;
  height: 400px;
  border-radius: var(--r-xl);
  overflow: hidden;
  border: 1px solid var(--hair-dark);
  background: radial-gradient(
      70% 60% at 62% 24%,
      rgba(201, 89, 40, 0.3) 0%,
      transparent 66%
    ),
    linear-gradient(168deg, #d9c9ab 0%, #c2ae8d 62%, #a8916e 100%);
}

@media (min-width: 1024px) {
  .svc-media {
    height: 520px;
  }
}

/* High-key variant because the photograph behind this changed. The anonymous
   hands it replaced measured lum 92; the technician shot measures 127, so
   keeping --grade-real here would have pushed this frame from 5.6% to 18.9%
   clipped and made the service floor read washed-out next to the bench cards
   below it, which are still mid-key. */
/* Measured, not eyeballed: the subject occupies rows 10%–74% of a square
   source, and this frame is landscape, so 331px of height is cropped. A
   centred crop starts at row 166 and decapitates a man whose head begins at
   row 108. 22% starts at row 73 — 35px of clearance above his head, with his
   hands and the opened MacBook still inside the bottom edge. */
.svc-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 22%;
  filter: var(--grade-real-hi);
}

.svc-beam {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: conic-gradient(
    from 200deg at 68% 18%,
    rgba(255, 255, 255, 0.09),
    transparent 22%,
    transparent 80%,
    rgba(201, 89, 40, 0.14)
  );
}

.svc-note {
  position: absolute;
  left: -18px;
  bottom: -22px;
  display: none;
  align-items: center;
  gap: 13px;
  padding: 16px 20px;
  border: 1px solid var(--hair-dark);
  border-radius: var(--r-md);
  background: rgba(255, 253, 248, 0.92);
  backdrop-filter: blur(14px);
  box-shadow: var(--e4);
}

@media (min-width: 900px) {
  .svc-note {
    display: flex;
  }
}

.svc-note-icon {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: var(--r-pill);
  background: linear-gradient(160deg, var(--ember-lift), var(--ember-deep));
  color: var(--on-ember);
  flex-shrink: 0;
}

.svc-note strong {
  display: block;
  font-size: 13px;
  font-weight: 500;
}

.svc-note span {
  font-size: 12px;
  color: rgba(42, 32, 24, 0.62);
}

/* Sohail's name tag, in the same form as the owner's under his portrait.
   .band-ink is a light sand ground, so the type is dark as it is there. */
.svc-tag {
  margin-top: 16px;
  padding-left: 14px;
  border-left: 2px solid var(--ember);
}

.svc-tag b {
  display: block;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--on-sand);
}

.svc-tag span {
  display: block;
  margin-top: 3px;
  font-size: 12px;
  color: rgba(42, 32, 24, 0.62);
}

/* ------------------------------------------------------------
   Owner's note
   ------------------------------------------------------------ */

/* Portrait leads on the left, which is the reverse of the service section
   below it. Two adjacent sections with text-left/photo-right would read as a
   template repeating itself. */
.owner-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 40px;
  align-items: start;
}

@media (min-width: 900px) {
  .owner-grid {
    grid-template-columns: 4fr 7fr;
    gap: 64px;
  }
}

@media (min-width: 1200px) {
  .owner-grid {
    gap: 88px;
  }
}

.owner-figure {
  margin: 0;
}

/* 4:5 is the portrait ratio, and object-position keeps the face in frame
   whatever the source crop turns out to be. */
/* 1:1, not the 4:5 an editorial portrait would normally want. Measured reason:
   the subject occupies 0%–91% of the frame width, so cropping the 216px a 4:5
   box needs would cut off either his hand on the counter or his shoulder. The
   photographer framed this square — his hand on the bench, the boxed stock on
   the shelves, the window light — and there is nothing in it to throw away.

   height:auto is load-bearing: without it the intrinsic height attribute wins
   as a presentational hint and aspect-ratio is ignored entirely. */
.owner-figure img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--r-lg);
  filter: var(--grade-real-hi);
  box-shadow: 0 0 0 1px rgba(20, 17, 14, 0.07), var(--e4);
}

.owner-figure figcaption {
  margin-top: 16px;
  padding-left: 14px;
  border-left: 2px solid var(--ember);
}

.owner-figure figcaption b {
  display: block;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
}

.owner-figure figcaption span {
  display: block;
  margin-top: 3px;
  font-size: 12px;
  color: rgba(20, 17, 14, 0.6);
}

.owner-body {
  max-width: 34rem;
}

/* Set larger and looser than body copy. A letter should not read at the same
   density as a product caption — the extra measure is what signals that a
   person is speaking rather than the brand. */
.letter {
  margin-top: 30px;
}

.letter p {
  font-size: 16.5px;
  line-height: 1.72;
  text-wrap: pretty;
  color: rgba(20, 17, 14, 0.8);
}

.letter p + p {
  margin-top: 17px;
}

/* The closing line is the commitment, so it carries full ink where the rest of
   the letter sits at 80%. */
.letter p:last-child {
  font-weight: 500;
  color: var(--on-paper);
}

.signoff {
  margin-top: 30px;
  padding-top: 22px;
  border-top: 1px solid var(--hair-light);
}

/* The display serif in italic reads as a signature without resorting to a
   handwriting face, which would be a costume. */
.signoff-name {
  display: block;
  font-style: italic;
  font-size: 27px;
  line-height: 1;
  color: var(--on-paper);
}

.signoff-role {
  display: block;
  margin-top: 9px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: rgba(20, 17, 14, 0.52);
}

/* ------------------------------------------------------------
   Proof — credentials + testimony
   ------------------------------------------------------------ */

/* .proof-card-critical and .quote-flag are gone with the one-star review they
   styled. Left as a note rather than dead rules, because unused selectors with
   confident comments attached are exactly how the stale nth-child that hid the
   mobile hamburger survived a rewrite. If a negative review returns here it
   should arrive with an owner response, which is a different component. */

.proof-grid {
  display: grid;
  grid-template-columns: 1fr;
  align-items: start;
  gap: 18px;
}

@media (min-width: 900px) {
  .proof-grid {
    grid-template-columns: 4fr 4fr 4fr;
  }
}

/* justify-content: space-between was correct when this grid held one credentials
   card and two long quotes of similar length. With six cards the grid stretches
   every row to its tallest member and space-between then flings a three-line
   quote to the top and its attribution to the floor, leaving a void in between.
   The voids were the design, not the content.

   Cards now hug their content (see `align-items: start` on the grid) and the
   attribution sits directly under the quote it belongs to. */
.proof-card {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: 22px;
  padding: 30px;
  border: 1px solid var(--hair-light);
  border-radius: var(--r-lg);
  background: #fffdfa;
  box-shadow: var(--sh-1);
  transition: border-color 0.5s var(--ease), transform 0.6s var(--ease),
    box-shadow 0.6s var(--ease);
}

.proof-card:hover {
  border-color: var(--hair-light-2);
  transform: translateY(-4px);
  box-shadow: var(--sh-2);
}

.quote {
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.45rem;
  line-height: 1.34;
  letter-spacing: -0.018em;
  text-wrap: pretty;
}

.attrib {
  display: flex;
  align-items: center;
  gap: 12px;
}

.mono-avatar {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  flex-shrink: 0;
  border-radius: var(--r-pill);
  background: var(--ink);
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--on-ink);
}

.attrib b {
  display: block;
  font-size: 13px;
  font-weight: 500;
}

/* Scoped to the text column: an unscoped `.attrib span` also matched the
   monogram, painting near-black initials onto a near-black disc. */
.attrib > div > span {
  font-size: 11.5px;
  color: rgba(20, 17, 14, 0.62);
}

.cert {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 15px 0;
}

.cert + .cert {
  border-top: 1px solid var(--hair-light);
}

.cert-mark {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border-radius: var(--r-sm);
  border: 1px solid var(--hair-light);
  background: var(--paper-2);
  color: var(--ember);
}

.cert b {
  display: block;
  font-size: 13.5px;
  font-weight: 500;
}

.cert span {
  font-size: 11.5px;
  color: rgba(20, 17, 14, 0.62);
}

.stars {
  display: flex;
  gap: 3px;
  color: var(--ember);
}

.score {
  display: flex;
  align-items: baseline;
  gap: 10px;
}

.score-val {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(2.6rem, 4vw, 3.6rem);
  letter-spacing: -0.012em;
  line-height: 0.9;
}

.score-cap {
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: rgba(20, 17, 14, 0.6);
}

/* ------------------------------------------------------------
   Floor — product row
   ------------------------------------------------------------ */

/* One card per row on a phone, not two.
   ------------------------------------------------------------
   At 375px a 2-up grid gives each card about 145px, so the photograph lands at
   roughly 113px and .prod-head — a flex row holding the name against the price
   — has no room to put them side by side. The name collapsed to a two-
   character column and interleaved with the price, which is the overlap in the
   screenshot.

   Widening the card is the right fix rather than stacking the name above the
   price, because the photograph was already too small to sell anything at
   113px. One per row gives it the full measure and lets .prod-head work as
   designed. Four picks is a short enough list to afford the height. */
.floor-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

@media (min-width: 560px) {
  .floor-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 900px) {
  .floor-grid {
    grid-template-columns: repeat(4, 1fr);
    gap: 22px;
  }
}

/* group block overflow-hidden rounded-[20px] p-4
   transition-all duration-500 hover:-translate-y-1
   background: var(--cream); border: 1px solid ink@7%

   The photograph is inset inside the card with its own 14px radius, not bled
   to the card edge. That inset is what lets the colour field read as a
   deliberate studio ground rather than as the card's own background. */
/* --paper stays, and the band moved instead.
   The card is the middle rung of the ladder that lets a white well read as a
   plate rather than a hole, and the mat rule is one rung above the band: this
   card sits on the --paper-2 floor band, so --paper is its rung.

   --bone was tried here first and measured badly — 83.57% band, 97.28% card,
   100.00% well, so the well was only 2.72 points above its own mat and
   vanished into it. --paper gives 83.57 -> 89.29 -> 100.00: the card reads as
   warm stock, the well reads as a plate inset into it.

   What was actually wrong before this pass was the BAND, not the card. The
   section was .band-bone at 97.28%, so a --paper card at 89.29% was darker
   than the surface it sat on — a warm block on a near-white ground, which is
   a plate inverted. Moving the band down to --paper-2 fixes the card without
   touching it. */
.prod {
  display: flex;
  flex-direction: column;
  padding: 16px;
  overflow: hidden;
  border: 1px solid rgba(20, 17, 14, 0.07);
  border-radius: var(--r-lg);
  background: var(--paper);
  transition: transform 0.5s var(--ease), border-color 0.5s var(--ease),
    box-shadow 0.5s var(--ease);
}

/* No aspect override needed: .packshot-well defaults to 1:1, which is both
   the aspect .prod-media held and the aspect every asset arrives in. The row
   mixes a laptop, a phone and a watch and a square well mats all three
   without favouring one. */

/* One lift value across the page. Four different hover distances read as four
   different authors. */
.prod:hover {
  transform: translateY(-4px);
  border-color: var(--hair-light-2);
  box-shadow: var(--sh-2);
}

/* overflow-hidden rounded-[14px] + a warm gradient behind the photograph, so
   a transparent or letterboxed source still sits on a lit ground. */
/* overflow-hidden rounded + a warm gradient behind the photograph, so a
   letterboxed source still sits on a lit ground. */
/* The gradient the two notes above are describing is the reason this rule had
   to go. "a warm gradient behind the photograph" is a coloured, graduated
   ground directly under an Apple product image, and the guidelines allow only
   "a white, off-white, light gray, or black background" and specifically
   forbid placing a product image "on a busy or textured background". It was
   solving for a letterboxed or transparent source; a verified packshot has a
   solid white background of its own, so the well matches it at #fff and the
   problem it was compensating for does not arise.
   .prod-media is retained below only as the ancestor of the .dev vector
   fallback, which no template currently emits. */

.prod-media .dev {
  position: relative;
  width: 68%;
  filter: drop-shadow(0 12px 14px rgba(20, 17, 14, 0.24))
    drop-shadow(0 30px 34px rgba(20, 17, 14, 0.16));
  transition: transform var(--dur-slow) var(--ease);
}

.prod-media .dev-wide {
  width: 86%;
}

.prod:hover .prod-media .dev {
  transform: translateY(-6px) scale(1.04);
}

/* mt-4 — the card carries the padding now, not the body. */
.prod-body {
  margin-top: 16px;
}

.prod-name {
  font-size: 14.5px;
  font-weight: 500;
  letter-spacing: -0.008em;
}

.prod-spec {
  margin-top: 4px;
  font-size: 12px;
  color: rgba(20, 17, 14, 0.62);
}

.prod-price {
  font-size: 14.5px;
  font-weight: 500;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

.prod-avail {
  margin-top: 4px;
  font-size: 11px;
  letter-spacing: 0.04em;
  color: rgba(20, 17, 14, 0.62);
  text-align: right;
}

/* ------------------------------------------------------------
   Stores + closing
   ------------------------------------------------------------ */

.close-wrap {
  position: relative;
  overflow: hidden;
}

.close-aura {
  position: absolute;
  inset: auto -20% -70% -20%;
  height: 130%;
  pointer-events: none;
  background: radial-gradient(
      44% 46% at 50% 88%,
      rgba(226, 78, 27, 0.16),
      transparent 72%
    ),
    radial-gradient(70% 50% at 50% 100%, rgba(20, 17, 14, 0.05), transparent 72%);
}

.close-inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 26px;
}

.close-inner .lede {
  max-width: 34rem;
  color: rgba(20, 17, 14, 0.66);
}

/* ------------------------------------------------------------
   Visit — the closing chapter
   ------------------------------------------------------------ */

.visit-grid {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  gap: 48px;
  align-items: center;
  padding-block: 88px;
}

@media (min-width: 1024px) {
  .visit-grid {
    grid-template-columns: 5fr 7fr;
    gap: 72px;
    padding-block: 118px;
  }
}

.visit-copy .chapter::after {
  background: rgba(255, 249, 245, 0.42);
}

.visit-facts {
  margin: 34px 0 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 22px 28px;
  padding-top: 26px;
  border-top: 1px solid rgba(255, 249, 245, 0.28);
}

.visit-facts dt {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 249, 245, 0.66);
}

.visit-facts dd {
  margin: 7px 0 0;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--on-ember);
}

.visit-facts a:hover {
  border-bottom: 1px solid currentColor;
}

/* This pill sits on a diagonal gradient, so its measured contrast moved with
   the section's height — it read 5.97:1 at one page length and 4.27:1 after the
   page grew by 24px, which is a fragile way to pass an accessibility threshold.
   A slightly darkened backing of its own makes the ratio a property of the
   component rather than of wherever the gradient happens to be underneath it. */
.visit-now {
  margin-top: 28px;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 8px 16px 8px 13px;
  border: 1px solid rgba(255, 249, 245, 0.38);
  border-radius: var(--r-pill);
  background: rgba(70, 26, 6, 0.34);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--on-ember);
}

.visit-now .pip-live {
  background: var(--on-ember);
  box-shadow: 0 0 0 0 rgba(255, 249, 245, 0.7);
}

/* Shut is stated, not shouted: a closed door is information, not a failure. */
.visit-now .pip-live,
[data-open-state].is-shut .pip-live {
  animation: none;
}

[data-open-state].is-shut {
  opacity: 0.78;
}

.visit-shot {
  position: relative;
  margin: 0;
}

/* The largest reproduction of the storefront on the page, and it was the worst
   graded — 52.6% of the building clipped to white at the exact moment the page
   asks someone to drive there. */
.visit-shot img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--r-xl);
  filter: var(--grade-real-sun);
  box-shadow: var(--e4-warm);
}

.visit-shot figcaption {
  margin-top: 14px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: rgba(255, 249, 245, 0.72);
}

/* Same reasoning as the assurance band: the rules made three addresses read as
   a table. Top and bottom hairlines are enough to seat the row. */
.stores {
  margin-top: 82px;
  display: grid;
  grid-template-columns: 1fr;
  gap: 4px 40px;
  border-top: 1px solid var(--hair-light);
  border-bottom: 1px solid var(--hair-light);
}

@media (min-width: 760px) {
  .stores {
    grid-template-columns: repeat(3, 1fr);
  }
}

.store {
  padding: 30px 0 32px;
}

.store h3 {
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.5rem;
  letter-spacing: -0.022em;
}

.store address {
  margin-top: 12px;
  font-style: normal;
  font-size: 13px;
  line-height: 1.62;
  color: rgba(20, 17, 14, 0.64);
}

.store-hours {
  margin-top: 16px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: rgba(20, 17, 14, 0.62);
}

.dot-open {
  width: 6px;
  height: 6px;
  border-radius: var(--r-pill);
  background: #2f8b52;
}

/* ------------------------------------------------------------
   Footer
   ------------------------------------------------------------ */

.foot {
  border-top: 1px solid var(--hair-dark);
  background: var(--paper-3);
  color: var(--on-sand);
  padding-block: 74px 34px;
}

.foot-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 46px;
}

@media (min-width: 1024px) {
  .foot-grid {
    grid-template-columns: 4fr 8fr;
    gap: 40px;
  }
}

.foot-cols {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 36px 24px;
}

@media (min-width: 760px) {
  .foot-cols {
    grid-template-columns: repeat(4, 1fr);
  }
}

.foot-col .eyebrow {
  margin-bottom: 18px;
  color: rgba(42, 32, 24, 0.52);
}

.foot-col li + li {
  margin-top: 11px;
}

.foot-col a {
  font-size: 13px;
  color: rgba(42, 32, 24, 0.74);
  transition: color 0.3s var(--ease);
}

.foot-col a:hover {
  color: var(--on-sand);
}

.foot-bar {
  margin-top: 60px;
  padding-top: 26px;
  border-top: 1px solid var(--hair-dark);
  display: flex;
  flex-direction: column;
  gap: 14px;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: rgba(42, 32, 24, 0.52);
}

@media (min-width: 760px) {
  .foot-bar {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
}

/* This row was the source of the cream strip down the right-hand edge.

   It holds Privacy, Terms and Accessibility, and each is now a .foot-soon span
   carrying a "soon" badge via ::after — roughly 138px each instead of a bare
   word. Three of those at a 24px gap cannot fit a phone, and with no
   flex-wrap the row simply overflowed: measured right edge 396px in a 375px
   viewport, which set document.scrollWidth to 396 and left 21px of body cream
   visible beside every in-flow section.

   It was invisible on wide screens and nearly invisible on a 390px phone (6px),
   which is exactly why it survived several rounds of mobile review. Wrapping is
   the fix; the guard against a recurrence is a test that checks element right
   edges rather than only document scrollWidth. */
.foot-bar nav {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 20px;
}

.foot-bar a:hover {
  color: var(--on-sand);
}

/* ------------------------------------------------------------
   Reveal system
   ------------------------------------------------------------ */

/* Entrance easing is decelerated: fast start, slow settle. Duration sits inside
   the 300-500ms band where motion reads as responsive rather than laggy.

   Wrapped in :where() so the reveal contributes zero specificity. Without it,
   `.rise.is-in { transform: none }` sits later in the cascade than every
   component and silently cancels their hover transforms — which is how a page
   ends up with only its loudest hover effect still firing. */
/* Retired. The staggered scroll reveal is off: every .rise element renders at
   full opacity immediately and the per-element --d delays do nothing.
   
   The reason is that it cost more than it bought. Content arriving 90ms,
   180ms, 260ms, 340ms after it scrolls into view reads as the page being slow
   rather than as the page being considered, and it is actively irritating on
   a second visit when you already know what is there. It also made the site
   hard to judge and hard to screenshot, because any capture during the
   staggered window shows half-faded type that looks like a rendering fault.
   
   The classes and the --d values stay in the markup, and deviceland.js still
   adds .is-in, so this is one rule to revert if the decision changes. Hover
   transitions, the masthead condense and the callbar are all untouched —
   this only removes the entrance stagger. */
:where(.rise),
:where(.rise.is-in) {
  opacity: 1;
  transform: none;
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .rise {
    opacity: 1;
    transform: none;
  }
}

/* ------------------------------------------------------------
   Service bench scene
   ------------------------------------------------------------ */

.bench {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: radial-gradient(
      64% 54% at 66% 16%,
      rgba(255, 186, 122, 0.22) 0%,
      transparent 68%
    ),
    linear-gradient(172deg, #2a241f 0%, #191512 46%, #0d0b09 100%);
}

.bench-cone {
  position: absolute;
  top: -12%;
  right: 8%;
  width: 62%;
  height: 110%;
  pointer-events: none;
  background: linear-gradient(
    186deg,
    rgba(255, 197, 138, 0.3) 0%,
    rgba(255, 168, 96, 0.1) 34%,
    transparent 74%
  );
  clip-path: polygon(38% 0%, 62% 0%, 100% 100%, 0% 100%);
  filter: blur(14px);
}

.bench-pool {
  position: absolute;
  left: 14%;
  right: 10%;
  bottom: 12%;
  height: 46%;
  pointer-events: none;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    rgba(255, 182, 118, 0.2),
    transparent 76%
  );
  filter: blur(18px);
}

.bench-vig {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    108% 88% at 62% 34%,
    transparent 34%,
    rgba(6, 5, 4, 0.72) 100%
  );
}

.bench-board {
  position: absolute;
  left: 12%;
  bottom: 16%;
  width: 74%;
  filter: drop-shadow(0 24px 34px rgba(0, 0, 0, 0.7));
}

.bench-tool {
  position: absolute;
  right: 8%;
  top: 16%;
  width: 34%;
  filter: drop-shadow(0 18px 26px rgba(0, 0, 0, 0.6));
}

.bench-dust {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.5;
  background-image: radial-gradient(
      1.5px 1.5px at 22% 34%,
      rgba(255, 214, 170, 0.85),
      transparent
    ),
    radial-gradient(1.5px 1.5px at 58% 20%, rgba(255, 214, 170, 0.7), transparent),
    radial-gradient(1px 1px at 74% 46%, rgba(255, 214, 170, 0.6), transparent),
    radial-gradient(1.5px 1.5px at 40% 62%, rgba(255, 214, 170, 0.5), transparent),
    radial-gradient(1px 1px at 86% 70%, rgba(255, 214, 170, 0.55), transparent),
    radial-gradient(1px 1px at 12% 76%, rgba(255, 214, 170, 0.45), transparent);
  animation: dust 9s var(--ease-io) infinite alternate;
}

@keyframes dust {
  0% {
    transform: translateY(0);
    opacity: 0.35;
  }
  100% {
    transform: translateY(-14px);
    opacity: 0.6;
  }
}

/* ============================================================
   Editorial apparatus
   Chapter numbers, footnotes and small-caps links: the typographic
   furniture of a catalogue rather than a landing page.
   ============================================================ */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.eyebrow {
  display: inline-flex;
  align-items: baseline;
}

/* A numbered chapter turns a scroll into an edition. */
.chapter {
  position: relative;
  margin-right: 15px;
  padding-right: 15px;
  font-variant-numeric: tabular-nums;
  color: var(--ember);
}

.chapter::after {
  content: "";
  position: absolute;
  right: 0;
  top: 50%;
  width: 7px;
  height: 1px;
  background: currentColor;
  opacity: 0.4;
}

.band-ink .chapter {
  color: var(--ember);
}

.band-ember .chapter {
  color: var(--on-ember);
}

/* inline-flex items-center gap-1.5 text-[13px] font-medium text-foreground/70 */
.link-store {
  gap: 6px;
  font-size: 13px;
  font-weight: 500;
  color: rgba(20, 17, 14, 0.7);
}

.link-store:hover {
  color: var(--on-paper);
}

/* Small-caps utility link, quieter than the headline it sits beside. */
.link-caps {
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.17em;
  text-transform: uppercase;
}

/* Footnote markers. Auditable claims read as engineering — but only if the
   marker is visible.

   At 0.52em these inherited from parents as small as 10.5px and rendered at
   5.5px. Measured across the page: 5.5, 5.5, 5.5, 5.5, 5.7, 5.7, 6.2, 6.8,
   7.0, 7.8, 7.8px. Nobody can see 5.5px type, which meant the single best
   credibility device on the page — every claim attributed to a checkable note
   — was invisible. max() puts a hard floor under it. */
.fn-ref {
  font-size: max(10px, 0.52em);
  font-weight: 600;
  font-feature-settings: normal;
  font-variant-numeric: normal;
  line-height: 0;
  vertical-align: super;
  margin-left: 2px;
  color: var(--ember-deep);
}

a .fn-ref,
.fn-ref:hover {
  color: var(--ember-deep);
}

.footnotes {
  counter-reset: fn;
  margin: 34px 0 0;
  padding: 26px 0 0;
  border-top: 1px solid var(--hair-dark);
  display: grid;
  gap: 9px 34px;
  list-style: none;
}

@media (min-width: 900px) {
  .footnotes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Measured at 126ch before the max-width. Dyson & Haselgrove (2001) put the
   optimum near 55ch and Nanavati & Bias (2005) cap it around 70; past that the
   return sweep starts landing on the wrong line. Set in ch so the limit tracks
   the font size instead of drifting from it. */
.footnotes li {
  position: relative;
  padding-left: 22px;
  max-width: 66ch;
  font-size: 11.5px;
  line-height: 1.62;
  color: rgba(42, 32, 24, 0.62);
  text-wrap: pretty;
}

.footnotes li::before {
  counter-increment: fn;
  content: counter(fn);
  position: absolute;
  left: 0;
  top: 1px;
  font-size: 9.5px;
  font-variant-numeric: tabular-nums;
  color: rgba(42, 32, 24, 0.58);
}

/* ============================================================
   Specialists
   ============================================================ */

.specialists {
  margin-top: 88px;
  padding-top: 34px;
  border-top: 1px solid var(--hair-dark);
}

.specialists-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 28px;
}

.spec-cards {
  display: grid;
  grid-template-columns: 1fr;
  gap: 14px;
}

@media (min-width: 760px) {
  .spec-cards {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* A 44px avatar is too small to prove anything and just large enough to look
   uncanny. These are 4:3 windows onto real work instead. */
.spec-card {
  border: 1px solid var(--hair-dark);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: rgba(255, 253, 248, 0.5);
  transition: border-color var(--dur-ui) var(--ease),
    background var(--dur-ui) var(--ease),
    transform var(--dur-reveal) var(--ease);
}

.spec-card:hover {
  border-color: var(--hair-dark-2);
  background: rgba(255, 253, 248, 0.78);
  transform: translateY(-3px);
}

.spec-card figure {
  margin: 0;
}

/* height:auto is load-bearing. The width/height attributes are kept for
   layout stability, but as presentational hints they also set the height
   property, which silently wins over aspect-ratio and renders a 1:1 source
   at full 1080px tall. */
.spec-card img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  filter: var(--grade-real);
  transition: transform var(--dur-slow) var(--ease);
}

.spec-card:hover img {
  transform: scale(1.04);
}

.spec-card figcaption {
  padding: 16px 18px 18px;
}

.spec-card b {
  display: block;
  font-size: 13.5px;
  font-weight: 500;
}

.spec-card figcaption span {
  display: block;
  font-size: 12px;
  line-height: 1.55;
  color: rgba(42, 32, 24, 0.66);
  margin-top: 4px;
}

/* ============================================================
   Floor cards — pricing, financing, arrival date
   ============================================================ */

.prod-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

/* The pricing column never shrinks; the name column does.

   Found by measurement, not by looking: at 375px .prod-pricing reported
   L286 R388 against a 375px viewport, so the price was hanging 13px off the
   screen and being clipped by `body { overflow-x: hidden }`. A price silently
   cut in half on the device carrying most of the traffic.

   .prod-head is a flex row with space-between, and both children default to
   min-width:auto — so neither could shrink below its content, and .prod-price
   and .prod-fin are both white-space:nowrap. The name is the side that can
   afford to wrap, so it gets min-width:0 and the money stays whole. */
.prod-head > :first-child {
  min-width: 0;
}

.prod-pricing {
  text-align: right;
  flex-shrink: 0;
}

/* A special, as Apple's store shows one: the price, then "Was $1,299 ·
   Save $39" in quiet type, then the end date. No percentage tag
   (inc/specials.php explains why). */
.prod-was {
  margin-top: 3px;
  font-size: 12px;
  color: rgba(20, 17, 14, 0.6);
  white-space: nowrap;
}

.prod-was del {
  text-decoration-thickness: 1px;
}

.prod-ends {
  margin-top: 2px;
  font-size: 11px;
  color: var(--ember-deep, #a8431a);
  white-space: nowrap;
}

.prod-fin {
  margin-top: 3px;
  font-size: 10.5px;
  color: rgba(20, 17, 14, 0.62);
  white-space: nowrap;
}

.prod-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 15px;
  padding-top: 12px;
  border-top: 1px solid var(--hair-light);
}

.prod-avail {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 11px;
  letter-spacing: 0.03em;
  color: rgba(20, 17, 14, 0.62);
}

/* "Carry it out", not a delivery date. The whole argument for driving to
   Hawthorne Blvd is that there is nothing to wait for. */
.prod-eta {
  font-size: 11px;
  letter-spacing: 0.03em;
  color: rgba(20, 17, 14, 0.78);
  white-space: nowrap;
}

/* ============================================================
   Stores — live floor state
   ============================================================ */

.store address a:hover {
  border-bottom: 1px solid currentColor;
}

.store-stock {
  margin-top: 10px;
  font-size: 11px;
  letter-spacing: 0.02em;
  color: rgba(20, 17, 14, 0.62);
}

/* ============================================================
   Footer — payment marks and legal
   ============================================================ */

.pay {
  margin-top: 54px;
  padding-top: 26px;
  border-top: 1px solid var(--hair-dark);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px 26px;
}

.pay-label {
  color: rgba(42, 32, 24, 0.56);
}

.pay-marks {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.pay-mark {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 27px;
  padding-inline: 11px;
  border: 1px solid var(--hair-dark);
  border-radius: var(--r-sm);
  background: rgba(255, 253, 248, 0.66);
  color: rgba(42, 32, 24, 0.74);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.08em;
}

/* Card brands as their own marks, in brand colour on white (owner, Oct 11):
   Simple Icons glyphs, each viewBox cropped to the glyph's real bounds so a
   wide wordmark is drawn at a readable size. */
.pay-mark:has(.pay-logo) {
  height: 32px;
  padding-inline: 10px;
  gap: 6px;
  background: #fff;
}

.pay-logo {
  display: block;
  flex: 0 0 auto;
}

.pay-word {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0;
  color: #1d1d1f;
}

.pay-mc {
  display: inline-flex;
}

.pay-mc i {
  display: block;
  width: 11px;
  height: 11px;
  border-radius: var(--r-pill);
}

.pay-mc i:first-child {
  background: #eb001b;
}

.pay-mc i:last-child {
  background: #f79e1b;
  margin-left: -4px;
}

.foot-legal {
  margin-top: 26px;
  padding-top: 22px;
  border-top: 1px solid var(--hair-dark);
  display: grid;
  gap: 10px;
}

/* Was 62rem at 10.5px — a 189-character measure, roughly 2.7x the readable
   maximum. Trademark small print is still text somebody may have to read. */
.foot-legal p {
  max-width: 68ch;
  font-size: 11.5px;
  line-height: 1.68;
  color: rgba(42, 32, 24, 0.6);
  text-wrap: pretty;
}

/* Pending destination. Reads as a real navigation item but is visibly not yet
   live, and is not a link, so it cannot mislead a visitor or a screen reader. */
.foot-soon {
  color: rgba(42, 32, 24, 0.42);
  cursor: default;
}

.foot-soon::after {
  content: "soon";
  margin-left: 7px;
  padding: 2px 5px;
  border: 1px solid rgba(42, 32, 24, 0.18);
  border-radius: var(--r-sm);
  font-size: 8.5px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  vertical-align: 1px;
  color: rgba(42, 32, 24, 0.5);
}

.foot-legal a {
  border-bottom: 1px solid rgba(42, 32, 24, 0.3);
}

.foot-legal a:hover {
  color: var(--on-sand);
}

@media (prefers-reduced-motion: reduce) {
  .nav-sheet-nav a {
    opacity: 1;
    transform: none;
  }
  .hero-photo {
    transform: scale(1.04);
  }
}




/* ============================================================
   THE HERO — the old panel's colour, the new layout's discipline
   ============================================================
   Replaces .hero-split. The decision that produced it, measured:

     white stage against the terracotta     6.69:1
     white stage against --paper            1.11:1

   A cream ground put the packshot on a 1.11:1 step and the product read flat
   against its own surround. The colour panel is worth six times that, and it
   is the one visual asset no competitor has.

   What the editorial version contributed, and what is kept here:
     - credentials anchored TOP, copy anchored BOTTOM, so the unavoidable
       slack collects in ONE expanse instead of pooling either side of a
       centred block. That was the single biggest flaw in every earlier hero.
     - both hairlines terminating on the same vertical.
     - the packshot on a true white stage, uncropped, nothing over it.
     - tracked small caps, squared geometry, ember used sparingly.
   ------------------------------------------------------------ */

/* Fills the viewport below the header, less the band, and grows past that
   when the copy needs more (see .hero-stack). The stage's photo never sets
   the height (contain below), so it shrinks to fit instead. vh first for
   browsers without svh. */
.hero-ed {
  position: relative;
  display: grid;
  grid-template-columns: 1fr;
  min-height: calc(100vh - var(--head-measured, var(--head-h)) - var(--hero-band-h, 186px));
  min-height: calc(100svh - var(--head-measured, var(--head-h)) - env(safe-area-inset-top, 0px) - var(--hero-band-h, 186px));
}

@media (min-width: 900px) {
  .hero-ed {
    /* 42/58. The panel needs enough width to hold the headline at display
       size without wrapping "Brilliance."; the stage takes the rest. */
    grid-template-columns: minmax(0, 42fr) minmax(0, 58fr);
  }
}

/* ---------- the terracotta panel ---------- */
.hero-ed-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: min(52svh, 420px);
  overflow: hidden;
  padding: clamp(20px, 3vh, 32px) clamp(26px, 2.6vw, 46px)
           clamp(24px, 4vh, 52px) clamp(26px, 3vw, 54px);
  background: linear-gradient(
    152deg,
    var(--ember-field) 0%,
    var(--ember-deep) 54%,
    var(--ember-deep-2) 100%
  );
  /* The inset vignette is the layer that makes a saturated field read as a lit
     surface rather than a swatch. */
  box-shadow: inset 0 0 170px rgba(38, 12, 0, 0.34);
  color: var(--on-ember);
}

.hero-ed-panel::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.06;
  background-image: var(--grain);
}

@media (min-width: 900px) {
  .hero-ed-panel {
    min-height: 420px;
  }
}

/* ---------- the year ----------
   14% --sand, not 10%. The panel is a gradient, and at 10% the numeral
   measured 1.228:1 against --ember-deep but only 1.135:1 against
   --ember-field — legibility varied across the field it sits on. 14% clears
   the old cream version's 1.113:1 at both ends.

   Its job is heritage, not structure: the original watermark existed to
   texture 28.6% of empty cream, and a saturated field does not need
   texturing. If it ever reads as clutter it can be deleted with no
   structural loss. */
.hero-ed-year {
  position: absolute;
  z-index: 0;
  /* POSITIVE. At left:-0.06em the leading 1 fell off the panel and the
     founding year rendered as "985" — a wrong number where a trust signal was
     intended. Vertical bleed is free; horizontal bleed deletes a digit. */
  left: 2%;
  top: 42%;
  translate: 0 -50%;
  font-family: var(--display);
  /* 26rem, not 17rem. At 17rem the cap bound from about 1430px upward, so the
     numeral stopped growing while the panel kept widening: 81% of the panel at
     1494 but only 63% at 1920. The vw term is tuned to the panel's own width
     (42vw) so the digits stay clear of its right edge at every size. */
  font-size: clamp(7rem, 19.5vw, 26rem);
  line-height: 0.74;
  letter-spacing: -0.04em;
  color: var(--sand);
  opacity: 0.14;
  pointer-events: none;
  user-select: none;
  white-space: nowrap;
}

/* "SINCE" above the numeral, as one lockup.
   A bare 1985 beside a Mac is genuinely ambiguous — the Macintosh launched in
   1984, so a reader can take the number for a product era or for Apple's own
   history rather than for this shop's founding. The credential line does say
   "since 1985", but it sits well above and the eye does not connect them.
   One word removes the ambiguity for the cost of one word.

   em units so it scales with the numeral, and --sans against the display face
   so it reads as a label on a graphic rather than part of the figure. */
.hero-ed-year i {
  display: block;
  margin-bottom: 0.06em;
  margin-left: 0.04em;
  font-family: var(--sans);
  font-style: normal;
  font-size: 0.082em;
  font-weight: 600;
  letter-spacing: 0.42em;
  line-height: 1;
  text-transform: uppercase;
}

/* ---------- credentials, anchored top ---------- */
.hero-ed-cred {
  position: relative;
  z-index: 2;
  padding-bottom: 11px;
  border-bottom: 1px solid rgba(255, 249, 245, 0.24);
}

.hero-ed-cred b {
  display: block;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: -0.004em;
  color: var(--on-ember);
}

.hero-ed-cred span {
  display: block;
  margin-top: 3px;
  font-size: 10.5px;
  letter-spacing: 0.04em;
  color: rgba(255, 249, 245, 0.66);
}

/* ---------- copy, anchored bottom ----------
   margin-top:auto is what concentrates the slack. Every earlier hero centred
   this block in a tall panel and got two voids; anchoring it low leaves one
   expanse above, which the year then occupies. */
.hero-ed-copy {
  position: relative;
  z-index: 2;
  margin-top: auto;
  padding-top: clamp(24px, 5vh, 60px);
}

.hero-ed-eyebrow {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--sand);
}

/* the rule terminates on the same vertical as the credential hairline above,
   because both are in the panel's padding box */
.hero-ed-eyebrow::after {
  content: "";
  height: 1px;
  flex: 1;
  background: rgba(255, 249, 245, 0.26);
}

.hero-ed-h1 {
  margin: 14px 0 0;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(2.6rem, 5vw, 5rem);
  line-height: 0.84;
  letter-spacing: -0.024em;
  color: var(--on-ember);
}

/* --sand, and it measures 4.88:1 on --ember-deep — clear of the 3:1 floor for
   display text at this size. Ember would be ember-on-ember. */
.hero-ed-h1 em {
  font-style: italic;
  color: var(--sand);
}

.hero-ed-lede {
  margin: 18px 0 0;
  /* min() so a 24rem measure cannot exceed a narrow panel's content box */
  max-width: min(24rem, 100%);
  font-size: 14.5px;
  line-height: 1.7;
  font-weight: 300;
  text-wrap: pretty;
  color: rgba(255, 249, 245, 0.84);
}

.hero-ed-cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 20px;
  margin-top: 24px;
}

/* "Touch display" beside the CTA (touch MacBook hero only). The fingertip
   presses and the ripples grow out of it three times after load, then the
   icon rests fully drawn. Every keyframe cycle starts and ends drawn: WebKit
   ended a 3-iteration run on the next cycle's first frame, and with arcs
   hidden there the icon rested as a bare finger (reviewer, Nov 7). No hover
   replay: leaving the hover restarts the load animation in CSS (a blink). */
.hero-ed-touch {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: rgba(255, 249, 245, 0.88);
}

.hero-ed-touch .dl-touch-glyph {
  flex: none;
  overflow: visible;
}

.hero-ed-touch .dl-touch-ripple,
.hero-ed-touch .dl-touch-hand {
  transform-box: view-box;
  transform-origin: 11px 11.5px;
}

.hero-ed-touch .dl-touch-ripple {
  animation: dl-touch-ripple 1.6s var(--ease, ease-out) 0.9s 3 both;
}

.hero-ed-touch .dl-touch-ripple.r2 {
  animation-delay: 1.05s;
}

.hero-ed-touch .dl-touch-hand {
  animation: dl-touch-press 1.6s var(--ease, ease-out) 0.9s 3 both;
}

@keyframes dl-touch-ripple {
  0%   { opacity: 1; transform: scale(1); }
  8%   { opacity: 0; transform: scale(0.45); }
  20%  { opacity: 0; transform: scale(0.45); }
  60%  { opacity: 1; transform: scale(1); }
  100% { opacity: 1; transform: scale(1); }
}

@keyframes dl-touch-press {
  0%, 100% { transform: translateY(0); }
  10%      { transform: translateY(1.2px); }
  25%      { transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-ed-touch .dl-touch-ripple,
  .hero-ed-touch .dl-touch-hand {
    animation: none;
  }
}

.hero-ed-tel {
  font-size: 13px;
  font-weight: 500;
  color: rgba(255, 249, 245, 0.88);
  transition: color var(--dur-ui) var(--ease);
}

.hero-ed-tel span {
  border-bottom: 1px solid rgba(255, 249, 245, 0.4);
  padding-bottom: 2px;
}

.hero-ed-tel:hover {
  color: var(--on-ember);
}

/* ---------- the white stage ----------
   #fff literally and full height, directly adjacent to the panel: that
   adjacency IS the 6.69:1 separation. A floating plane would have needed cream
   around it and put the product back on a 1.11:1 step. */
.hero-ed-stage {
  position: relative;
  display: grid;
  /* asset row, then caption row. The asset can never enter the caption's
     track, so "nothing on the product photo" is guaranteed by the layout
     rather than by arithmetic that only held at one viewport height. */
  grid-template-rows: minmax(0, 1fr) auto;
  justify-items: center;
  min-height: min(46svh, 380px);
  padding: clamp(24px, 3.5vh, 44px) 8% clamp(14px, 2vh, 20px);
  background: #fff;
}

@media (min-width: 900px) {
  .hero-ed-stage {
    min-height: 420px;
    contain: size;
  }
}

/* Percentage width so the product tracks the stage. A fixed rem cap is what
   made it shrink from 67% to 30% of its panel between 1512 and 3440. */
/* THE GRID ITEM IS THE <picture>, NOT THE <img>.
   deviceland_hero_art() emits <picture><img class="hero-art-img"></picture>, so
   grid-row and align-self on the img applied to a child of the grid item and
   did nothing. The picture took the default align-self:stretch, filled row 1,
   and the img sat at its top — leaving the product high with a large void
   beneath it. Measured at 1494 the asset had 32px above and 163px below.

   The earlier compliance probe reported "asset on white: inside" and passed,
   because the asset WAS inside its stage. It simply was not centred, and the
   probe never asked. That check is now part of the audit. */
.hero-ed-stage > picture {
  grid-row: 1;
  justify-self: stretch;
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-width: 0;
  min-height: 0;
}

/* max-* on both axes with auto sizing: the packshot scales to fit whichever
   constraint binds first, uncropped and undistorted, and can never grow taller
   than its track. */
.hero-ed-stage .hero-art-img,
.hero-ed-stage img {
  display: block;
  max-width: min(100%, 64rem);
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

/* A square catalogue packshot as the hero (Nov 7, the touch MacBook Pro):
   positioned from its measured product box (--fw/--fh: product width/height
   as fractions of the image, --cx/--cy: its centre, --ar: image aspect) so the
   PRODUCT takes at most 88% of the stage on either axis. What falls outside
   the container is the photo's own white canvas on a white stage: the
   approved packshot exception. The product is never clipped and the file is
   untouched (deviceland_hero_art). */
.hero-ed-stage > .hero-art-fit {
  grid-row: 1;
  justify-self: stretch;
  align-self: stretch;
  position: relative;
  overflow: hidden;
  min-width: 0;
  min-height: 0;
  container-type: size;
}

.hero-ed-stage .hero-art-fit .hero-art-trim {
  --h: min(calc(88cqh / var(--fh)), calc(88cqw / (var(--fw) * var(--ar))));
  position: absolute;
  max-width: none;
  max-height: none;
  width: calc(var(--h) * var(--ar));
  height: var(--h);
  left: calc(50cqw - var(--cx) * var(--h) * var(--ar));
  top: calc(50cqh - var(--cy) * var(--h));
  object-fit: fill;
}

/* BESIDE the asset, never on it. Bottom-right of the stage, under a hairline,
   clear of the image box — which the probe measures rather than assumes. */
.hero-ed-cap {
  grid-row: 2;
  justify-self: end;
  padding-top: 10px;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--muted-fg);
}

/* ---------- phones ----------
   Stacked, and the year comes down hard: at 19vw on a 390px screen it is
   74px, which is neither architectural nor subtle — just a number sitting
   behind the text. The panel's colour already carries the field there. */
@media (max-width: 899px) {
  /* Measured on a 375px screen: at 34vw the numerals are 105px tall and were
     centred at 34% of the panel, which put them across BOTH the eyebrow
     (249-264) and the top of the headline (278-348). The clear band between
     the credential rule and the eyebrow is only 62px, so at that size the
     mark cannot avoid the copy — it has to be smaller to be seen at all.

     19vw fits the band with air on either side, and the opacity comes up from
     0.10 to 0.14 because a smaller mark needs the contrast to register. A
     crisp small "SINCE 1985" reads as a credential; a huge half-obscured one
     reads as a mistake. The architectural treatment survives from 900px up,
     where there is a genuine expanse for it to occupy. */
  /* 17vw at 24%, not 19vw at 23.5%: at the larger size the mark's top edge
     landed exactly on the credential rule (both at y=187 on a 375px screen),
     so the rule appeared to run through "SINCE 1985" and read as a stray
     line. The clear band between that rule and the eyebrow is 62px, and a
     53px mark sits inside it with air at both ends. */
  .hero-ed-year {
    font-size: 17vw;
    opacity: 0.14;
    top: 24%;
  }

  /* The credential rule comes off on phones.
     ------------------------------------------------------------
     On a wide panel it is a folio device: a hairline closing the heritage
     block before the offer begins. On a 375px panel there is a second
     full-width rule 62px below it — the eyebrow's — and the 1985 mark sits
     between the two. Three horizontal elements inside 110px stops reading as
     a catalogue and starts reading as ruled paper, which is why it looked
     like a stray line rather than a device.

     The block below is still separated: by the mark, by the eyebrow rule that
     does the same job, and by the padding kept here. */
  .hero-ed-cred {
    border-bottom: 0;
  }

  .hero-ed-copy {
    padding-top: clamp(30px, 8vh, 70px);
  }

  .hero-ed-stage {
    padding-inline: 12%;
  }
}

@media (max-width: 560px) {
  .hero-ed-cta {
    gap: 14px;
  }

  .hero-ed-cta .btn {
    width: 100%;
    justify-content: center;
  }
}

/* the rail's ember tick — the entry point the three cells were missing */
.hb-tick {
  display: block;
  width: 14px;
  height: 2px;
  margin-bottom: 11px;
  background: var(--ember);
  flex: 0 0 auto;
}


/* ------------------------------------------------------------
   Full-bleed chrome
   ------------------------------------------------------------
   .shell caps content at --shell (1480px) and centres it. That is right for
   the reading chapters below, and wrong for the header and the hero's rail:
   both sit against a full-bleed hero, and at 1920 they measured 77% of the
   viewport against the hero's 99% — an inset bar over an edge-to-edge
   composition.

   The padding deliberately equals .hero-ed-panel's padding-left, so the
   wordmark, the hero copy and the rail's first cell share one vertical. */
.masthead-inner.shell,
.hero-band-inner.shell {
  max-width: none;
  margin-inline: 0;
  padding-inline: clamp(26px, 3vw, 54px);
}


/* The tick belongs ABOVE the label, not beside it. .hb-cell is display:flex
   from the icon layout this replaced, which made the tick and the text
   siblings in a row — measured, the tick sat 28px left of "Trade in". */
.hero-band-facts .hb-cell {
  display: block;
}

.hero-band-facts .hb-cell > span {
  display: block;
}


/* ------------------------------------------------------------
   The fact tick — one marker for every credential cell on the page
   ------------------------------------------------------------
   An audit found 27 action glyphs and 9 decorative pictograms. The action
   glyphs stay: a magnifier, a bag, an arrow and a map pin are affordances a
   shopper looks for, and no word replaces them in the space available.

   The 9 decorations were pictograms whose only job was to sit beside a label —
   the assurance cells, the two proof badges, the service portrait, the
   trade-in sparkle. Those are the "feature list" pattern the rule was actually
   about, and the rule had only ever been applied to the hero band, leaving one
   component speaking a different language from the rest of the page.

   A 14x2px ember rule instead. It gives each cell the entry point it needs and
   it speaks the language already here — the folio's dividers, the credential
   hairlines, the eyebrow rules. Same element as .hb-tick in the hero band, so
   the two registers finally match. */
.fact-tick {
  display: block;
  width: 14px;
  height: 2px;
  margin-bottom: 11px;
  background: var(--ember);
  flex: 0 0 auto;
}

/* Cells that laid their icon out as a flex row need to stack now, or the tick
   sits beside the label instead of above it — the same defect the hero band's
   ticks had until .hb-cell was made a block. */
.assure-cell,
.proof-badge,
.spec-card figure + div {
  display: block;
}


/* .cert-mark was a 40x40 boxed icon chip. Swapping its pictogram for the ember
   tick left the box behind, so the credential rows rendered as a beige square
   with a small dash floating in it — which looks like a missing icon rather
   than a deliberate marker.

   The box goes. In a list the tick works as a left-hand rule against the
   label, the same way it works above a label in the assurance and rail cells;
   what has to go is the container that framed it as an icon. margin-bottom is
   zeroed because .fact-tick carries 11px for its stacked use, which would
   push it off centre in a row. */
.cert-mark {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  width: auto;
  height: auto;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}

.cert-mark .fact-tick {
  margin-bottom: 0;
}


/* One folio behaviour on every page.
   ------------------------------------------------------------
   The collapse used to hang off .is-stuck, which shop pages have from the
   server (data-pinned) — so the credentials vanished before they were seen and
   those pages had to keep the folio on permanently. The header then behaved
   one way on the homepage and another on a product page.

   .is-scrolled comes from scroll position alone, so both pages agree: the
   folio is present on arrival and collapses once reading starts. */
.masthead.is-scrolled .folio,
.masthead[data-pinned].is-scrolled .folio {
  height: 0;
  opacity: 0;
  border-bottom-color: transparent;
}

.masthead[data-pinned]:not(.is-scrolled) .folio {
  height: var(--folio-h);
  opacity: 1;
  border-bottom-color: var(--hair-light);
}

/* Admin bar pinned on small screens; see the note in chrome.css. The homepage
   needs its own copy because chrome.css is not loaded on this template. */
@media screen and (max-width: 600px) {
  body.admin-bar #wpadminbar {
    position: fixed;
    top: 0;
  }
}

/* ------------------------------------------------------------
   Header chrome: the homepage copies of three shop-page fixes
   ------------------------------------------------------------
   chrome.css is not loaded on this template, so every fix to the shared
   header has to be made here as well. Three in a row were missed this way:
   the folio's surface, the admin bar, and the nav sheet's clearance. */

/* The railing needs its own ground. The island is positioned inside
   .masthead-inner and so only ever covered the logo row; the folio sat below
   it with `background: transparent`, and the ember hero ran straight through
   the credentials — "Forty-one years on Hawthorne" printed over "APPLE
   PREMIER PARTNER". Same values as the island so the two rows read as one
   bar. */
.folio {
  background: rgba(248, 242, 232, 0.92);
  backdrop-filter: blur(22px) saturate(1.6);
  -webkit-backdrop-filter: blur(22px) saturate(1.6);
}

/* The sheet's first row cleared --head-h-stuck, which is the condensed BAR
   height and smaller than the header actually standing there, so the search
   field was drawn over the folio. Measured values instead of tokens. */
.nav-sheet .nav-sheet-inner {
  padding-block: calc(var(--dl-admin-offset, 0px) + var(--head-measured, 108px) + 18px)
    calc(40px + env(safe-area-inset-bottom, 0px));
}

/* The call bar covers the sheet's own last rows, so it stands down while the
   sheet is open. */
body:has(#nav-sheet.is-open) .callbar,
body.nav-open .callbar {
  opacity: 0;
  pointer-events: none;
}

/* ------------------------------------------------------------
   Bag drawer: the homepage copies of the chrome.css fixes
   ------------------------------------------------------------
   Measured at 375x780 on the homepage with seven items: the panel grew to
   1072px and the footer buttons sat at 1016 — off the screen, unreachable —
   while the same drawer on /shop/ was correct at 780px. chrome.css is not
   loaded on this template, so the flex chain that bounds the item list never
   applied here. Same three rules, same reasoning as chrome.css. */
.dl-drawer .dl-drawer-panel {
  min-height: 0;
}

.dl-drawer .dl-drawer-panel .dl-drawer-body {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.dl-drawer .dl-drawer-panel .widget_shopping_cart_content {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

.dl-drawer .dl-drawer-panel .nasa-minicart-items {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

.dl-drawer .dl-drawer-panel .nasa-minicart-footer,
.dl-drawer .dl-drawer-panel .woocommerce-mini-cart__total,
.dl-drawer .dl-drawer-panel .woocommerce-mini-cart__buttons {
  flex: 0 0 auto;
}

.dl-drawer {
  top: var(--dl-admin-offset, 0px);
}

/* The stepper as a grid, so the digit is centred whatever Elessi sizes the
   input to; measured 25x18 inside a 48px cell before this. */
.dl-drawer .dl-drawer-panel .quantity {
  display: grid;
  grid-template-columns: 1fr 28px;
  grid-template-rows: 1fr 1fr;
  width: 78px;
  height: 46px;
}

.dl-drawer .dl-drawer-panel .quantity input.qty,
.dl-drawer .dl-drawer-panel .quantity input[type="number"],
.dl-drawer .dl-drawer-panel .quantity input[type="text"] {
  grid-column: 1;
  grid-row: 1 / span 2;
  width: 100%;
  height: 100%;
  min-width: 0;
  box-sizing: border-box;
  padding: 0;
  line-height: normal;
  text-align: center;
}

.dl-drawer .dl-drawer-panel .quantity .plus,
.dl-drawer .dl-drawer-panel .quantity .minus {
  position: static;
  grid-column: 2;
  width: auto;
  height: auto;
  min-height: 0;
}

.dl-drawer .dl-drawer-panel .quantity .plus {
  grid-row: 1;
}

.dl-drawer .dl-drawer-panel .quantity .minus {
  grid-row: 2;
}

/* Pending state while the server confirms a new subtotal. */
.dl-bag-update-state {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-left: auto;
  font-family: var(--sans);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted-fg);
}

.dl-bag-update-state[hidden] {
  display: none;
}

.dl-bag-update-state i {
  width: 13px;
  height: 13px;
  border: 1.5px solid var(--hair-light-2);
  border-top-color: var(--ember-deep);
  border-radius: 50%;
  animation: dl-update-spin 0.7s linear infinite;
}

@keyframes dl-update-spin {
  to { transform: rotate(360deg); }
}

.dl-drawer-panel.is-updating .quantity {
  opacity: 0.45;
  pointer-events: none;
}

html body .dl-drawer-panel.is-updating .woocommerce-mini-cart__total .amount,
html body .dl-drawer-panel.is-updating .mini-cart-total .amount {
  opacity: 0.3;
}

.dl-drawer-panel .quantity,
.dl-drawer-panel .woocommerce-mini-cart__total .amount {
  transition: opacity var(--dur-micro) var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  .dl-bag-update-state i {
    animation: none;
    border-color: var(--ember-deep);
  }
}

/* Legal links beside the copyright (Terms, Returns), as on apple.com.
   Kept in chrome.css and deviceland.css alike: the footer is on both. */
.foot-legal {
  margin-left: 18px;
}

.foot-legal a {
  margin-right: 16px;
  color: inherit;
  text-decoration: none;
}

.foot-legal a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Bag line facts (inc/reserve.php, deviceland_bag_line_facts; owner, Oct 24):
   the AppleCare+ part and Ready to ship / Additional processing time, each on
   its own row under the options. Same rules in chrome.css and bag.css. */
.dl-drawer dl.variation dd.variation-Part,
.dl-drawer dl.variation dd.variation-AppleCarepart,
.dl-drawer dl.variation dd.variation-Availability {
  display: block;
  margin-top: 3px;
}

.dl-drawer dl.variation dd.variation-Part::after,
.dl-drawer dl.variation dd.variation-AppleCarepart::after,
.dl-drawer dl.variation dd.variation-Availability::after,
.dl-drawer dl.variation dd:has(+ dt.variation-Part)::after,
.dl-drawer dl.variation dd:has(+ dt.variation-AppleCarepart)::after,
.dl-drawer dl.variation dd:has(+ dt.variation-Availability)::after {
  content: "";
}

.dl-drawer .dl-ship {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 500;
}

.dl-drawer .dl-ship.is-ready {
  color: var(--ember-deep);
}

.dl-drawer .dl-ship.is-ready::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ember);
}

/* Apple's AppleCare+ mark before "AppleCare+ part …" (owner, Oct 24): small,
   the tile alone, a hairline on its rounded shape. */
.dl-drawer .dl-line-fact {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.dl-drawer .dl-applecare-mark {
  height: 18px;
  width: 18px;
  max-width: none;
  margin: 0;
  display: inline-block;
  vertical-align: middle;
  flex: none;
  object-fit: contain;
  border-radius: 23%;
  box-shadow: 0 0 0 0.5px rgba(22, 19, 15, 0.14);
}

.dl-drawer .dl-line-fact {
  align-items: flex-start;
}
.dl-drawer .dl-part {
  white-space: nowrap;
}

/* Each fact on its own full row: the details list is a flex row in the
   drawer and checkout, where a short "Ready to ship" sat beside the
   AppleCare+ line (owner's screenshot, Oct 24). */
.dl-drawer dl.variation dd.variation-Part,
.dl-drawer dl.variation dd.variation-AppleCarepart,
.dl-drawer dl.variation dd.variation-Availability {
  flex: 0 0 100%;
  width: 100%;
  max-width: 100%;
}

/* Vertical rhythm: the AppleCare+ row sat 3px under the line above with a
   tile taller than the text; 7px above it, 5px above availability, and an
   18px tile that sits inside the line. */
.dl-drawer dl.variation dd.variation-Part,
.dl-drawer dl.variation dd.variation-AppleCarepart {
  margin-top: 7px;
}

/* "Part" (Oct 25) is now the first fact under the options; the AppleCare+
   line after it is a step down, as availability is. */
.dl-drawer dl.variation dd.variation-Part ~ dd.variation-AppleCarepart {
  margin-top: 5px;
}

/* A line with no options (an accessory): "Part" is the first detail, right
   under the name, so it takes no gap of its own (owner, Oct 25: the drawer
   read as padded around it; the bag page's own spacing was right). */
.dl-drawer dl.variation dd.variation-Part:first-of-type {
  margin-top: 0;
}

.dl-drawer dl.variation dd.variation-Availability {
  margin-top: 5px;
}

.dl-drawer img.dl-applecare-mark,
.dl-drawer dl.variation img.dl-applecare-mark {
  width: 18px;
  height: 18px;
}

/* The drawer's option list is a wrapping row of whole values, set here
   rather than inherited: Elessi's style.css (dl.variation flex + wrap) is
   not loaded on the homepage, where the list fell back to plain text and
   broke values mid-value ("40W USB-C / Power Adapter", Oct 24). */
.dl-drawer dl.variation {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
}

/* Elessi's own rules reach inside this list on inner pages: its global `p`
   sets Jost on every value and a cart rule paints the values #555. One face
   and one taupe for every spec line, as on the homepage (Oct 24). */
html body .dl-drawer dl.variation dd,
html body .dl-drawer dl.variation dd p {
  font-family: inherit;
  text-transform: none;
  color: var(--muted-fg);
}

/* Spec lines wrap as whole values with a dot BETWEEN values, never at the
   start or end of a line (owner, Oct 24; "512GB SSD ·" used to end a line).
   Each dot sits in the 14px gap to the LEFT of its value; the list is shifted
   left by that gap and clipped there, so the dot in front of the first value
   on every line is never drawn. Same rule as the bag and checkout (bag.css). */
html body .dl-drawer dl.variation {
  margin-left: -14px;
  clip-path: inset(-6px -6px -6px 12px);
}

html body .dl-drawer dl.variation dd {
  position: relative;
  display: block;
  margin-left: 14px;
}

html body .dl-drawer dl.variation dd::after,
html body .dl-drawer dl.variation dd:has(+ dt)::after {
  content: none;
}

html body .dl-drawer dl.variation dd::before {
  content: "\00b7";
  display: block;
  position: absolute;
  top: 0;
  left: -14px;
  width: 14px;
  text-align: center;
  color: rgba(20, 17, 14, 0.35);
}

html body .dl-drawer dl.variation dd.variation-Part,
html body .dl-drawer dl.variation dd.variation-AppleCarepart,
html body .dl-drawer dl.variation dd.variation-Availability {
  flex: 0 0 calc(100% - 14px);
}

html body .dl-drawer dl.variation dd.variation-Part::before,
html body .dl-drawer dl.variation dd.variation-AppleCarepart::before,
html body .dl-drawer dl.variation dd.variation-Availability::before {
  content: none;
}
