/* ==========================================================================
   Advanced Packaging Company LLC
   Design system + shared shell + Contact page.

   DIRECTION CONTRACT
   THESIS:         An industrial data plate, not a marketing contact page. The one
                   idea: reaching a real person in Detroit is the product feature,
                   so the page refuses the category default of a contact form and
                   spends its whole hierarchy on the direct paths instead.
   OWN-WORLD:      Off-white working ground, dark ink chrome, brand orange as the
                   single action color. Caps-tracked micro labels over generous
                   values, set like equipment labeling. Depth from real layered
                   elevation, never from color washes. The other five brand colors
                   are withheld here and reserved for product identity.
   STORY:          The visitor understands this is a real Detroit manufacturer,
                   believes a quote reaches a person rather than a queue, and sends
                   the RFQ email or dials.
   FIRST VIEWPORT: One dominant H1 left at the narrow measure, a lead line under it,
                   the orange quote action immediately below, the data plate of
                   address, phone, and email beneath that, and the elevated map
                   panel holding the right half on desktop.
   FORM:           Asymmetric split, brief-pinned from SWIPE-LIST move 2. Incomplete
                   brand path: palette and logos inherited, system expanded here.

   Token rules: every dimension below is named. No arbitrary one-off values in the
   component layers. Components reference the semantic layer, not raw brand hexes.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. FONTS, self-hosted. Zero third-party requests, no Google connection.
   Open Sans v44, latin subset, from google-webfonts-helper.

   TODO CONFIRM VISUALLY: the brand note "Condensed Bold" is ambiguous between the
   Open Sans Condensed family (a separate download) and a bold weight styled tight.
   Open Sans 800 with negative tracking is the placeholder. Because headings route
   through --font-heading, swapping the family later is a one-line change.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: 'Open Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('assets/fonts/open-sans-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Open Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('assets/fonts/open-sans-600.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Open Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('assets/fonts/open-sans-700.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Open Sans';
  font-style: normal;
  font-weight: 800;
  font-display: swap;
  src: url('assets/fonts/open-sans-800.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --------------------------------------------------------------------------
   2. TOKENS
   -------------------------------------------------------------------------- */
:root {
  color-scheme: light;

  /* --- Brand palette. Site design itself. ------------------------------- */
  --brand-orange: #E15126;
  --brand-pink: #D87FB4;
  --brand-yellow: #F1E91A;
  --brand-red: #E22726;
  --brand-gold: #E1A526;
  --brand-green: #1D4635;

  /* --- Product family accents. -----------------------------------------
     A product may use the color that matches the product even when it is not
     in the brand palette. --product-blue exists because there is no blue in
     the brand palette and BlueZone needs a real one. Green and purple are the
     true fluid colors, not the brand forest green.
     Not used on Contact. Defined here so Products inherits one source. */
  --product-blue: #1B5FA8;
  --product-green: #4E9A2F;
  --product-purple: #6B3FA0;
  --product-orange: var(--brand-orange);
  /* No longer var(--brand-red). The palette red #E22726 is L 0.5886 C 0.2199 in
     OKLCh, and at full-screen scale on the RedZone detail field that much chroma
     at that lightness reads as glare rather than as a colour. Deepened one step,
     L x0.96 and C x0.88, holding hue: L 0.564, C 0.193, h 27.4, a 0.18 degree
     hue shift and no gamut clipping.

     Both axes move rather than one. Chroma alone was tested and is the wrong
     lever: at constant lightness it costs white-text contrast (4.61 to 4.55)
     while barely reducing the intensity. Lightness alone keeps the full 0.22
     chroma and stays glaring.

     C 0.193 against an sRGB ceiling near 0.22 for this hue keeps it emphatically
     red, nowhere near dusty, and L 0.564 is well clear of maroon.

     Diverging from the brand palette here is sanctioned rather than a drift:
     CLIENT-CONTEXT allows a product's own accent to differ from the palette, and
     --product-blue, --product-green and --product-purple already do. Stored as
     hex because every other colour token in this file is hex; the OKLCh work is
     how the value was derived, not how it is kept. */
  --product-red: #CF332E;
  --product-yellow: var(--brand-yellow);

  /* --- Neutrals. Dark industrial ink, green shifted so it sits with the
     brand green rather than reading as a cold black. --------------------- */
  --ink: #16211D;
  --ink-deep: #0E1714;
  --ink-raised: #1E2C27;
  /* The one true neutral, and the single exception to the green shift above. It
     exists for exactly one pairing: ink on the OrangeZone field. --ink-deep is
     H160 at 24% saturation, a desaturated green-cyan, which sits 149 degrees from
     the orange field's H14, close enough to complementary that the two neutralise
     toward grey-brown along the antialiased glyph edges and read muddy. At 0%
     saturation this introduces no competing hue, and it also lifts the ratio from
     4.70 to 5.41. Orange only: every other family keeps the green-shifted ink or
     white, because the cohesion that shift buys is deliberate and this is a fix
     for one hue collision, not a change of mind about the palette. */
  --ink-pure: #000000;
  --off-white: #F7F5F1;
  --white: #FFFFFF;
  --grey-100: #EDEAE4;
  --grey-300: #D6D1C8;
  --grey-500: #8B8880;
  --grey-700: #4A4E4A;

  /* --- Semantic layer. Components use these, not the raw hexes. ---------
     Contrast measured against the ground each is used on:
       --color-text on --color-bg .................. 15.6:1
       --color-text-muted on --color-bg ............  7.8:1
       --color-accent-deep on --color-bg ...........  5.4:1
       --color-on-action on --color-action .........  5.84:1
       --color-text-muted-on-dark on --ink-deep ....  8.4:1
     --grey-500 is 3.06:1 on the ground, so it is borders and large text only,
     never body copy.

     The action surface is deliberately NOT --color-accent. On the bright brand
     orange nothing legible survives: white is 3.88:1 and even --ink-deep, the
     darkest neutral, only reaches 4.70:1, a technical pass that reads as a
     smudge rather than as a label. Deepening the surface one step to
     --color-accent-deep buys white text at 5.84:1, which is the same orange
     family, a real 1.3x margin over the 4.5:1 floor, and a button whose label
     is the brightest thing on it.
     --color-accent stays the bright brand orange, but it is now a structural
     color only: focus ring, footer rule, nav underline, map pin. It never
     carries text. Anything that does carry text uses the action pair. */
  --color-bg: var(--off-white);
  --color-surface: var(--white);
  --color-surface-sunken: var(--grey-100);
  --color-surface-dark: var(--ink-deep);
  --color-surface-dark-raised: var(--ink-raised);
  --color-text: var(--ink);
  --color-text-muted: var(--grey-700);
  --color-text-on-dark: #F2F0EC;
  --color-text-muted-on-dark: #A8B5AE;
  --color-border: var(--grey-300);
  --color-border-strong: var(--grey-500);
  --color-border-on-dark: rgba(242, 240, 236, 0.16);
  /* Glass, for the trust cards on the dark hero. Not new colors: the same
     #F2F0EC as --color-text-on-dark and --color-border-on-dark, at lower alpha.
     Kept this faint deliberately. At 0.045 the fill lifts the ground by about 9
     luminance levels, which is enough to read as a panel edge-on but not enough
     to become a button, and the label on top still measures over 14:1. If these
     ever read as heavy UI the fix is to lower these two numbers, nothing else. */
  --glass-fill: rgba(242, 240, 236, 0.045);
  --glass-border: rgba(242, 240, 236, 0.10);
  /* Hover, still the same #F2F0EC, still only alpha. The fill goes from 0.045 to
     0.07 and the edge from 0.10 to 0.18: enough that the card lifts under the
     cursor, far short of becoming a control. */
  --glass-fill-hover: rgba(242, 240, 236, 0.07);
  --glass-border-hover: rgba(242, 240, 236, 0.18);
  --color-accent: var(--brand-orange);
  --color-accent-deep: #B33D18;
  /* The one pair every text-bearing accent surface uses: primary button, skip
     link. Keeping it a named pair means a future surface cannot pick up the
     orange without also picking up the foreground that passes on it. */
  --color-action: var(--color-accent-deep);
  --color-on-action: var(--white);
  /* The same action orange for surfaces where the orange IS the ink rather than
     the fill, on a dark ground. Not a new colour: it is --brand-orange, already
     in the palette and already the focus ring.

     The two are a light/dark pair and the reason is symmetric. --color-action is
     deepened precisely so white reads on it as a FILL on light ground: it gets
     5.84:1 there where brand orange manages only 3.88:1. Invert the ground and
     the requirement inverts with it. Measured on the --ink-deep header:

       --color-action  #B33D18  3.12:1   fails 4.5:1 as a label, passes as border
       --color-accent  #E15126  4.70:1   passes as both

     So an outline button drawn in --color-action would have unreadable text on
     ink. Dark contexts take this token instead. */
  --color-action-on-dark: var(--color-accent);
  /* The single gap between an interior page head and the block below it. See
     the .page-head rule for why this is a token and not three paddings that
     happen to agree. Steps up with the rest of the desktop rhythm at 60rem. */
  --page-head-gap: var(--sp-6);
  --color-focus: var(--brand-orange);

  /* --- Spacing scale. Every gap, pad, and section rhythm maps here. ----- */
  --sp-1: 0.25rem;
  --sp-2: 0.5rem;
  --sp-3: 0.75rem;
  --sp-4: 1rem;
  --sp-5: 1.5rem;
  --sp-6: 2.5rem;
  --sp-7: 4rem;
  --sp-8: 6rem;

  /* --- Type scale. Display sizes are fluid so type breathes with the
     window rather than jumping at a breakpoint. Display caps at 5.5rem,
     under the 6rem ceiling. ---------------------------------------------- */
  /* 14px floor. Nothing on the site sets type below --fs-100, so a label or a
     caption cannot drift into the 11px to 13px range that fails legibility for
     the older trade buyers this audience actually contains. */
  --fs-100: 0.875rem;
  --fs-200: 0.9375rem;
  --fs-300: 1rem;
  --fs-400: 1.0625rem;
  --fs-500: 1.1875rem;
  --fs-600: clamp(1.375rem, 1.2rem + 0.7vw, 1.625rem);
  --fs-700: clamp(1.75rem, 1.45rem + 1.4vw, 2.5rem);
  --fs-800: clamp(2.75rem, 1.9rem + 4vw, 5.5rem);
  /* The Home hero display size, and the only place this is used. Set in two
     lines across the container's full 1152px content width.
     4.75rem, not the 4.5rem the single-column version used, and the two-column
     layout is what pays for it. In a 30rem text column this sets in FOUR lines
     at 298px rather than two lines at 141px, and that taller block is the point:
     it is the text mass that plays against the product mass beside it. The
     cluster sits inside the text block's vertical span rather than beneath it,
     so those extra lines cost the hero no height at all.
     Why not 5rem: measured against the real metrics, four lines need a column of
     at least 459px at 4.75rem but 483px at 5rem. The column is 480px, so 5rem
     lands on the wrong side of the 4/5-line boundary and did in fact render five
     lines, which pushed the trust strip 25px past the fold. 4.75rem clears the
     boundary by 21px, which is margin rather than luck. */
  --fs-900: clamp(2.75rem, 1.5rem + 4.5vw, 4.75rem);

  /* [MOBILE] The contact plate's value line. This one is sized by a string, not
     by the type scale: info@advancedpackagingcompany.com is 33 characters with
     no break opportunity, and it is measured, not guessed. Open Sans 600 sets it
     at 19.074em, and --tracking-tight takes off 0.01em per character, so the run
     is 18.744em. The plate is full-bleed inside the container on phones, so the
     space it has is 100vw minus both --sp-5 gutters, 48px.
     4.4vw is that relationship expressed as a slope. --fs-100 floors it, which
     is the site's 14px body floor and NOT a value the email is allowed to drop
     below: at 320px the slope gives 14.08px and lays the address out in 263.9px
     inside 272px, so the narrowest phone in circulation clears it with 8.1px to
     spare without the floor ever being reached. The floor only engages under a
     318px viewport, which is narrower than any shipping phone.
     --fs-500 caps it, one step under the desktop --fs-600, so the block reads as
     the same plate, just smaller. */
  --fs-plate-value: clamp(var(--fs-100), 4.4vw, var(--fs-500));

  --lh-display: 0.98;
  --lh-heading: 1.16;
  --lh-body: 1.6;

  /* Tracking floor is -0.04em. Display stays above it at -0.03em. */
  --tracking-display: -0.03em;
  --tracking-tight: -0.01em;
  --tracking-label: 0.1em;

  --weight-body: 400;
  --weight-medium: 600;
  --weight-bold: 700;
  --weight-heading: 800;

  --font-sans: 'Open Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-heading: var(--font-sans);

  /* --- Layout system. --------------------------------------------------- */
  --container: 75rem;
  --container-narrow: 37rem;
  --col-primary: 30rem;
  --measure: 68ch;
  --gutter: var(--sp-5);
  /* One header height at every width. The logo is the trimmed full lockup,
     952x794, so 1.199:1. Height drives it, width follows: 48px tall renders
     57.6px wide, which the mobile bar absorbs easily.
     The lockup contains the wordmark, so it needs more height than a badge
     would to stay legible. 48px in the 72px bar leaves 12px above and below. */
  --header-h: 4.5rem;
  --logo-h: 3rem;
  --logo-h-footer: 3.75rem;
  --tap-min: 2.75rem;
  --tap-sm: 2.25rem;
  --tap-lg: 3.25rem;
  --focus-offset: 3px;
  /* Hamburger geometry. The closed-to-X shift is derived from the bar gap plus
     the bar thickness, so the X stays centered if either token changes. */
  --bar-gap: 5px;
  --bar-shift: calc(var(--bar-gap) + var(--border-thick));
  --bar-inset: 0.6rem;
  /* SVG user units inside the 0 0 400 400 viewBox, NOT CSS pixels. What the
     visitor actually reads is this value times the panel's scale factor, so the
     floor has to be checked against the rendered size, not against the number.
     The major road was widened to a 36-unit stroke to hold the label, so the
     type is no longer the thing that has to give: 17 units puts the full em box
     (about 23 units) inside the stroke with roughly 6 units of white on each
     flank. See the road note in the map section.
     19 rather than 17 because of the worst case, which is not the phone: at the
     60rem breakpoint the primary column takes its fixed --col-primary and the
     map gets the remainder, so the panel renders about 304px against a 400-unit
     viewBox and scales the label DOWN by 0.76. 19 units survives that at 14.4
     CSS px, and every wider or narrower viewport renders it larger. One value,
     no breakpoint override. Below about a 360px viewport the panel gets small
     enough to push it under the floor again; that is the accepted limit. */
  --map-label-fs: 19px;

  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.875rem;
  --radius-pill: 100vmax;
  --border-hairline: 1px;
  --border-thick: 2px;

  /* Inline link underline. One recipe for every inline link that reveals a rule
     on hover, so phone, email, and address links never drift apart. A hairline
     sitting close under the text, not a highlighter bar under a heading. */
  --underline-thickness: 1px;
  --underline-offset: 0.16em;

  /* --- Elevation. Real depth: every shadow carries an offset and a soft
     blur, layered in two or three stops. Tinted with the ink hue rather
     than pure black so it sits on the warm ground. ------------------------ */
  --shadow-rgb: 22 33 29;
  --shadow-sm:
    0 1px 2px rgb(var(--shadow-rgb) / 0.10),
    0 2px 6px rgb(var(--shadow-rgb) / 0.07);
  --shadow-md:
    0 2px 4px rgb(var(--shadow-rgb) / 0.10),
    0 8px 20px rgb(var(--shadow-rgb) / 0.13);
  --shadow-lg:
    0 3px 6px rgb(var(--shadow-rgb) / 0.11),
    0 16px 36px rgb(var(--shadow-rgb) / 0.16),
    0 40px 72px -24px rgb(var(--shadow-rgb) / 0.22);
  /* The primary action carries real elevation at rest, in the same layered
     language as the cards and the map panel. Tinted with the accent hue so it
     reads as the orange lifting off the ground rather than a grey drop shadow
     sitting under it. */
  --shadow-accent-rgb: 150 44 12;
  --shadow-accent:
    0 2px 4px rgb(var(--shadow-accent-rgb) / 0.20),
    0 8px 18px rgb(var(--shadow-accent-rgb) / 0.26);
  /* Hover is a 2px lift, so the shadow should tighten and darken rather than
     bloom. The third stop is gone: a 48px ambient stop under a 2px move reads
     as fog, not as elevation. Two stops, both closer than the resting pair
     relative to the distance travelled. */
  --shadow-accent-hover:
    0 2px 4px rgb(var(--shadow-accent-rgb) / 0.24),
    0 6px 12px rgb(var(--shadow-accent-rgb) / 0.30);
  /* The product jug is a transparent cutout, so box-shadow is the wrong tool: it
     would draw under the rectangular image box instead of under the bottle.
     drop-shadow follows the alpha channel, so this is the only way the jug reads
     as standing ON the color field rather than pasted over it. Two stops, same
     offset-plus-blur language as the rest of the elevation scale. Deliberately
     heavier than --shadow-md, because it has to separate a white bottle from a
     bright yellow tile without help from a border. */
  --shadow-jug:
    drop-shadow(0 2px 3px rgb(var(--shadow-rgb) / 0.24))
    drop-shadow(0 16px 24px rgb(var(--shadow-rgb) / 0.30));

  /* --- Motion. Front-loaded ease out, exponential feel. ----------------- */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
  --dur-fast: 140ms;
  --dur-base: 260ms;
  --dur-slow: 500ms;
  /* Entrance only. Kept separate from --dur-slow so tuning the reveal cannot
     silently slow down every other transition on the site. */
  --dur-reveal: 500ms;
  --reveal-shift: 1rem;
  --reveal-stagger: 70ms;
  /* The Home hero cluster only. It travels further and lasts longer than the
     generic entrance because it is the page's single first-paint moment rather
     than a section arriving mid-scroll. Three outward steps of 90ms plus 700ms
     settles the whole cluster at 970ms, inside one second.
     Per-jug travel distance lives on the family classes as --lift, because the
     focal jug should move furthest and the ones behind it least, which is how
     parallax behaves and reinforces the depth. */
  --cluster-stagger: 90ms;
  --dur-cluster: 700ms;
  /* The focal jug's height, and the single value the whole cluster derives from.
     Every other jug is --s of this, rises --r of it, and casts a shadow scaled
     from it, so the cluster stays one proportional system at every width.

     This base value is the MOBILE one, per mobile-first; the desktop fan sets its
     own in the 60rem block.
     Two terms. The first fills a third of the usable width, since mobile is two
     rows of three, with a 1.12 factor so adjacent jugs overlap rather than sit
     in tidy cells.

     The second term used to be 14svh, chosen to force all six jugs above the
     fold. That was the wrong trade: it BOUND on every phone, so the width term
     never applied and the cluster rendered at 118px with dead space either side,
     reading as a cramped huddle rather than a product mass. 30svh is deliberately
     loose enough that the WIDTH term wins on any normal phone, which is what
     makes the cluster fill its space: the focal jug goes 118px to 182px, up 54%.
     It stays only as a backstop for very short viewports.
     A slightly taller hero that needs a small scroll is the better trade.

     The inset is 4rem rather than the 3rem gutter pair. Backing the cluster off
     the screen edges by 8px a side takes about 5% off the jugs, which is the
     "few percent" that buys breathing room, and it stops the mass running
     literally edge to edge, which was most of why the screen read as packed.
     Still 95% of the usable width, so the product mass is intact. */
  --cluster-h: min(calc((100vw - 4rem) / 3 * 1.4243 * 1.12), 30svh);
  --lift-sm: -2px;
  --lift-md: -4px;
  --press-scale: 0.97;
}

/* --------------------------------------------------------------------------
   2b. FAMILY THEME  [BOTH]
   One pair per product family, read by three surfaces: the Products tile, the
   family detail page, and the colour dot in the nav dropdown. Defined on an
   attribute rather than in each component so a tile and the page it links to
   cannot drift apart, which they would the moment the hexes were restated.

   --fam-field is the colour. --fam-on is the only foreground measured against
   it. Ratios, measured not estimated:
     orange 4.70 : green 10.61 : red 5.04 : yellow 14.25 : blue 6.46 : purple 6.61
     Red measured 4.61 against the palette red, the tightest of the six and only
     0.11 above the floor. See --product-red for why it was deepened.

   THE RULE: --fam-field is a FIELD, never body text. As text on the off-white
   page ground it fails on four of the six (orange 3.57, red 4.24, yellow 1.18,
   purple 2.53). Green passes at 9.74 and blue at 5.94, which is the trap: use
   either as text here and the pattern looks fine on this page and silently
   breaks on four of the five clones. Colour goes in the field, text stays on
   the ink neutrals, and the action colour stays orange sitewide.
   -------------------------------------------------------------------------- */
/* Orange is the one family whose on-field ink is not --ink-deep. See --ink-pure:
   the green-shifted ink is near-complementary to this field and reads muddy on it.
   Scoped here rather than at the component, so the Products tile and the detail
   page cannot disagree about it. */
[data-family='orange'] { --fam-field: var(--product-orange); --fam-on: var(--ink-pure);  }
[data-family='green']  { --fam-field: var(--brand-green);    --fam-on: var(--white);    }
[data-family='red']    { --fam-field: var(--product-red);    --fam-on: var(--white);    }
[data-family='yellow'] { --fam-field: var(--product-yellow); --fam-on: var(--ink-deep); }
[data-family='blue']   { --fam-field: var(--product-blue);   --fam-on: var(--white);    }
[data-family='purple'] { --fam-field: var(--brand-pink);     --fam-on: var(--ink-deep); }

/* --fam-panel is the staging surface behind the detail-page jug. It is the family
   colour DEEPENED, never the family colour covered.

   That distinction is the whole point. An off-white glass over the field was the
   obvious move and it is measurably wrong: at 0.10 alpha it costs 8.5% of the
   colour on purple, 10.4% on blue and 8.8% on orange, and even at a barely visible
   0.06 it still costs 5 to 6% on those three. Chroma measured in OKLCh, which is
   where "does this wash the colour" is an answerable question.

   A lightness-only move cannot desaturate, because C and h are held. Measured
   round trip through sRGB, chroma retained: green 98.3%, yellow 95.9%, orange
   100.2%, red 100.0%, blue 100.7%, purple 100.5%. Yellow is the only one under 98
   because at L 0.911 it is near the top of the sRGB gamut and clips slightly.

   The panel lands 1.09 to 1.18 against the field: a frame you feel, not a slab.
   It also improves the white jug's silhouette on every family, most where it was
   worst: yellow 1.19 to 1.41, purple 2.56 to 3.01 which crosses the 3:1 line.

   Fallback is the plain field, so where relative colour syntax is unsupported the
   panel is still defined by its inset edge and its contact shadow and nothing
   looks broken. It just loses the tonal frame. */
[data-family] {
  --fam-panel: var(--fam-field);
}

@supports (background: oklch(from red l c h)) {
  [data-family] {
    --fam-panel: oklch(from var(--fam-field) calc(l * 0.94) c h);
  }
}

/* Breakpoints used throughout, kept here as the single reference since custom
   properties cannot be used inside a media query condition:
     small  40rem  / 640px
     mid    52rem  / 832px
     large  60rem  / 960px */

@media (min-width: 60rem) {
  :root {
    --gutter: var(--sp-6);
  }
}

/* --------------------------------------------------------------------------
   3. BASE
   -------------------------------------------------------------------------- */
*,
*::before,
*::after { box-sizing: border-box; }

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

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  font-family: var(--font-sans);
  font-size: var(--fs-400);
  font-weight: var(--weight-body);
  line-height: var(--lh-body);
  color: var(--color-text);
  background: var(--color-bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: var(--weight-heading);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}

p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
/* dd carries a UA margin-inline-start of about 40px, which indents a definition
   away from its own term. Reset here in the shared base so any description list
   on any page starts flush, rather than each component remembering to do it. */
dl, dt, dd { margin: 0; }
img, svg { display: block; max-width: 100%; }
img { height: auto; }

a {
  color: inherit;
  text-decoration-color: rgb(var(--shadow-rgb) / 0.3);
  text-underline-offset: 0.2em;
}

address { font-style: normal; }

:focus-visible {
  outline: var(--border-thick) solid var(--color-focus);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-sm);
}

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

/* The 1px values below are the fixed clip recipe for hiding content visually
   while leaving it available to a screen reader. They are not design
   dimensions, so they are deliberately not tokenized. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: var(--sp-4);
  top: var(--sp-2);
  z-index: 100;
  transform: translateY(-200%);
  padding: var(--sp-3) var(--sp-4);
  background: var(--color-action);
  color: var(--color-on-action);
  font-size: var(--fs-200);
  font-weight: var(--weight-bold);
  border-radius: var(--radius-md);
  transition: transform var(--dur-base) var(--ease-out);
}
.skip-link:focus { transform: translateY(0); }

/* Micro label. The data plate grammar: a tracked caps label over a generous
   value. This is a deliberate system used for the contact fields only, not an
   eyebrow stamped over every section. */
.label {
  display: block;
  font-size: var(--fs-100);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* --------------------------------------------------------------------------
   4. BUTTONS
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  min-height: var(--tap-min);
  padding: var(--sp-3) var(--sp-5);
  font-family: inherit;
  font-size: var(--fs-300);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  text-decoration: none;
  border: var(--border-hairline) solid transparent;
  border-radius: var(--radius-md);
  cursor: pointer;
  /* A wrapped label would grow the header row taller instead of wider, which
     is a worse failure than a narrow button. */
  white-space: nowrap;
  transition:
    transform var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out),
    background-color var(--dur-fast) var(--ease-standard),
    border-color var(--dur-fast) var(--ease-standard);
}

/* .btn__label carries no styles on purpose. It exists so every button label is a
   real element rather than a bare text run, which in a flex container becomes an
   anonymous flex item that no selector can reach. Nothing needs it today; it is
   kept because it costs nothing and it means any future label-level rule can be
   written once instead of only working on the buttons that happened to have a
   span. An optical translateY lived here briefly and was removed: the label is
   already centred in its box and the shift was addressing the wrong cause. */

/* White on the deepened orange, 5.84:1. See the action-pair note in the token
   block for why this is not the bright --color-accent. */
.btn--accent {
  background: var(--color-action);
  color: var(--color-on-action);
  box-shadow: var(--shadow-accent);
}

/* [BOTH] Tier two. Same orange system, one step quieter: the colour draws the
   outline instead of filling it. Use where an action is real but is not the
   page's primary conversion, the header CTA being the case that prompted it.
   Two filled orange buttons on one screen cancel each other out, and on a phone
   the header and hero CTAs are both visible at once.

   Frame and label are SEPARATE variables, and on dark ground they deliberately
   disagree. The orange is what ties this button to the action system, so the
   border stays orange everywhere. But orange as a LABEL on ink reads heavy and
   muddy even at a passing ratio, so on dark the label goes to the same off-white
   the rest of the header uses and lets the frame carry the colour. The two jobs
   have different requirements and get different values:

     dark ground   border --color-action-on-dark   4.70:1   frame, needs 3.0
                   label  --color-text-on-dark    16.03:1   text, needs 4.5
     light ground  border --color-action           5.84:1
                   label  --color-action           5.84:1

   On light ground orange text is legible and keeps the tie visible, so both
   stay orange there. Making the border white as well would turn this into a
   generic ghost button with no relationship to the action colour, which is the
   one thing this variant must not become.

   No shadow. The glow is the primary button's signature and lending it to tier
   two would flatten the very distinction this variant exists to make. */
.btn--secondary {
  --btn-secondary-frame: var(--color-action);
  --btn-secondary-label: var(--color-action);
  background: transparent;
  border-color: var(--btn-secondary-frame);
  color: var(--btn-secondary-label);
  box-shadow: none;
}

.site-header .btn--secondary,
.hero .btn--secondary,
.site-footer .btn--secondary {
  --btn-secondary-frame: var(--color-action-on-dark);
  --btn-secondary-label: var(--color-text-on-dark);
}

/* Press feedback is unconditional: a tap must register on touch too. */
.btn:active { transform: scale(var(--press-scale)); }

/* Hover lift is gated, because a touch tap fires hover and would otherwise
   leave the control stuck in its lifted state after the finger leaves. */
@media (hover: hover) and (pointer: fine) {
  .btn--accent:hover {
    transform: translateY(var(--lift-sm));
    box-shadow: var(--shadow-accent-hover);
  }

  /* Tier two resolves INTO tier one on hover: the outline fills. It is the most
     legible state change available without inventing a treatment, it borrows the
     same --lift-sm the primary uses so the two share one hover dialect, and the
     filled pair is already proven at 5.84:1.

     The fill is --color-action on every ground, dark included. Only the resting
     ink needed the light/dark flip, because only the resting state puts orange
     on the ground as text; once filled, the foreground is white on orange and
     the ground behind it no longer participates.

     Still no glow. Filling is the feedback; the shadow stays tier one's alone. */
  .btn--secondary:hover {
    background: var(--color-action);
    border-color: var(--color-action);
    color: var(--color-on-action);
    transform: translateY(var(--lift-sm));
  }
}

.btn--lg {
  min-height: var(--tap-lg);
  padding: var(--sp-4) var(--sp-6);
  font-size: var(--fs-500);
}

/* --------------------------------------------------------------------------
   5. SITE HEADER  [BOTH]
   Dark ink bar, white logo, orange quote action. Sticky, and it gains real
   elevation only once the page has scrolled off zero.
   -------------------------------------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--color-surface-dark);
  border-bottom: var(--border-hairline) solid var(--color-border-on-dark);
  transition: box-shadow var(--dur-base) var(--ease-out);
}
.site-header[data-scrolled='true'] { box-shadow: var(--shadow-md); }

/* The elevation shadow exists to lift the bar off LIGHT content scrolling under
   it. Measured, --shadow-md lays a 20px grey ramp on whatever is below, peaking
   about 39 levels off the off-white ground.

   That is correct over a light page and wrong over a dark full-bleed band: the
   ramp runs out of light to fall on and terminates against the band's hard black
   edge, which reads as a muddy smear rather than a seam. Worse, the shadow is
   effectively invisible ON the band itself (ink on ink), so in that moment it is
   producing nothing but the smudge.

   So the shadow is suppressed while a dark band is passing under the bar, and
   only then. Every light page keeps it at full strength. The header already
   transitions box-shadow, so it fades rather than popping.
   script.js owns the data-over-dark flag, from the same rAF-throttled scroll
   tick that already sets data-scrolled. */
.site-header[data-scrolled='true'][data-over-dark='true'] { box-shadow: none; }

/* [MOBILE] Background scroll lock while the drawer is open. The fixed position
   is what makes this hold on iOS Safari, where overflow:hidden on the body is
   ignored. script.js preserves and restores the scroll offset around it. */
body.nav-open {
  position: fixed;
  inset-inline: 0;
  width: 100%;
  overflow: hidden;
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--header-h);
}

.site-header__brand {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  margin-inline-end: auto;
  text-decoration: none;
  transition: opacity var(--dur-fast) var(--ease-standard);
}
.site-header__brand:hover { opacity: 0.82; }
/* Height drives the size, width follows, so the 1:1 mark can never be squashed
   if the asset is ever swapped for a different ratio. */
.site-header__brand img {
  height: var(--logo-h);
  width: auto;
}

.site-header__cta {
  flex: 0 0 auto;
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-100);
  min-height: var(--tap-sm);
}
/* The outline this element used to carry as a one-off mobile override is now
   .btn--secondary in the markup, at every width. This rule sets size only. */

/* The long label is the default. The compressed one only appears on the
   narrowest phones, see the header budget note below. */
.site-header__cta-short { display: none; }

/* --- Primary nav --------------------------------------------------------- */
.site-nav__list {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
}

.site-nav__link {
  position: relative;
  display: block;
  padding: var(--sp-2) 0;
  font-size: var(--fs-300);
  font-weight: var(--weight-medium);
  color: var(--color-text-muted-on-dark);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-standard);
}
.site-nav__link:hover { color: var(--color-text-on-dark); }

/* Underline rule grows from the left on hover and sits permanently under the
   current page. */
.site-nav__link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: var(--border-thick);
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease-out);
}
.site-nav__link:hover::after { transform: scaleX(1); }
.site-nav__link[aria-current='page'] {
  color: var(--color-text-on-dark);
  font-weight: var(--weight-bold);
}
.site-nav__link[aria-current='page']::after { transform: scaleX(1); }

/* A detail page is inside the Products section without being the Products page,
   so the parent gets a section marker rather than aria-current, which belongs
   on the one link that is actually the current page. */
.site-nav__link[data-section='current'] { color: var(--color-text-on-dark); }

/* --- Products dropdown  [BOTH] -------------------------------------------
   The link stays a link and the disclosure is a separate button, so "go to
   Products" and "show the families" never compete for the same click. The
   chevron is rendered at BOTH widths on purpose: hiding it on desktop would
   park aria-expanded on a display:none element, where it describes nothing.

   Closed state is display:none, and that is load bearing. focusables() in
   script.js filters on getClientRects().length, and a visibility:hidden or
   max-height:0 element is still laid out and still returns rects, so either
   would drop six invisible links into the open drawer's Tab trap. The drawer
   itself may use visibility:hidden when closed only because focusables() is
   never called while it is shut.
   ------------------------------------------------------------------------- */
/* Only the one item that owns a panel needs the containing block, so the other
   six list items stay plain <li> with no class to carry. */
.site-nav__item--has-sub {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.site-nav__disclosure {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* [MOBILE] A real 36px tap target, because in the drawer this button is the
     only way to expand the panel. On desktop it is resized, see the note in
     the 60rem block: there the whole item opens the menu on hover and focus,
     so the button does not have to carry the target on its own. */
  width: var(--tap-sm);
  height: var(--tap-sm);
  padding: 0;
  /* Belongs with the padding reset. Some UA sheets give button a horizontal
     margin, and a margin here would be a second spacing system stacked on the
     list's gap, which is the thing this component must not do. */
  margin: 0;
  background: none;
  border: 0;
  color: var(--color-text-muted-on-dark);
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: color var(--dur-fast) var(--ease-standard);
}
.site-nav__disclosure:hover { color: var(--color-text-on-dark); }

.site-nav__chevron {
  width: 0.6em;
  height: 0.6em;
  border-right: var(--border-thick) solid currentColor;
  border-bottom: var(--border-thick) solid currentColor;
  /* Rotated a quarter turn past level so the two borders read as a chevron
     pointing down. The translate re-centres the glyph inside its own box,
     which rotating alone throws off. */
  transform: translateY(-25%) rotate(45deg);
  transition: transform var(--dur-base) var(--ease-out);
}
[data-open='true'] > .site-nav__disclosure .site-nav__chevron {
  transform: translateY(10%) rotate(225deg);
}

.site-subnav {
  display: none;
}
.site-nav__item--has-sub[data-open='true'] .site-subnav {
  display: grid;
}

.site-subnav__link {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  position: relative;
  padding: var(--sp-3) 0;
  font-size: var(--fs-200);
  color: var(--color-text-muted-on-dark);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-standard);
}
.site-subnav__link:hover { color: var(--color-text-on-dark); }
.site-subnav__link[aria-current='page'] {
  color: var(--color-text-on-dark);
  font-weight: var(--weight-bold);
}

/* The same left-origin accent wipe the top level nav uses, so the dropdown
   speaks the one hover dialect rather than inventing a second. */
.site-subnav__link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: var(--sp-2);
  width: 100%;
  height: var(--border-thick);
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease-out);
}
.site-subnav__link:hover::after { transform: scaleX(1); }

/* The dot is the family colour, read from the same [data-family] pair the tile
   and the detail page use. It is decoration only: the family name sits right
   beside it, so colour is never the sole carrier of meaning. */
.site-subnav__dot {
  flex: 0 0 auto;
  width: 0.75rem;
  height: 0.75rem;
  background: var(--fam-field);
  border-radius: 50%;
}

/* --- Hamburger  [MOBILE] ------------------------------------------------- */
.nav-toggle {
  flex: 0 0 auto;
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--bar-gap);
  width: var(--tap-min);
  height: var(--tap-min);
  padding: 0 var(--bar-inset);
  background: transparent;
  border: var(--border-hairline) solid var(--color-border-on-dark);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-standard);
}
.nav-toggle:hover { border-color: var(--color-text-muted-on-dark); }

.nav-toggle__bar {
  display: block;
  height: var(--border-thick);
  width: 100%;
  background: var(--color-text-on-dark);
  border-radius: var(--radius-sm);
  transition:
    transform var(--dur-base) var(--ease-out),
    opacity var(--dur-fast) var(--ease-standard);
}
.nav-toggle[aria-expanded='true'] .nav-toggle__bar:nth-child(1) {
  transform: translateY(var(--bar-shift)) rotate(45deg);
}
.nav-toggle[aria-expanded='true'] .nav-toggle__bar:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded='true'] .nav-toggle__bar:nth-child(3) {
  transform: translateY(calc(var(--bar-shift) * -1)) rotate(-45deg);
}

/* [MOBILE] Nav is a drawer under the bar. Closed by default.
   Opacity and transform only, so the drawer runs on the compositor. An earlier
   max-height approach animated layout and needed a guessed height ceiling,
   which also made the perceived speed wrong for short menus. */
@media (max-width: 59.999rem) {
  .site-nav {
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    background: var(--color-surface-dark);
    border-bottom: var(--border-hairline) solid var(--color-border-on-dark);
    box-shadow: var(--shadow-lg);
    opacity: 0;
    transform: translateY(calc(var(--sp-2) * -1));
    visibility: hidden;
    transition:
      opacity var(--dur-fast) var(--ease-standard),
      transform var(--dur-base) var(--ease-out),
      visibility 0s linear var(--dur-base);
  }
  .site-nav[data-open='true'] {
    opacity: 1;
    transform: translateY(0);
    visibility: visible;
    transition:
      opacity var(--dur-base) var(--ease-out),
      transform var(--dur-base) var(--ease-out),
      visibility 0s;
  }
  /* Expanded, the drawer holds 13 links plus the CTA. Measured against a
     375x667 phone that is about 672px of content into 595px of available
     height, so it overflows and has to scroll. dvh first because the mobile
     URL bar makes vh lie; vh stays as the fallback for anything without dvh.
     overscroll-behavior stops the scroll chaining to the body, which is pinned
     with position:fixed while the drawer is open. */
  .site-nav {
    max-height: calc(100vh - var(--header-h));
    max-height: calc(100dvh - var(--header-h));
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .site-nav__list {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--sp-2) var(--gutter) var(--sp-4);
  }

  /* The link and its chevron share the first row, the panel wraps onto the
     second at full width. The row rule moves from the link to the item so it
     spans both controls rather than stopping short of the chevron. */
  .site-nav__item--has-sub {
    flex-wrap: wrap;
    border-bottom: var(--border-hairline) solid var(--color-border-on-dark);
  }
  .site-nav__item--has-sub > .site-nav__link {
    flex: 1 1 auto;
    border-bottom: 0;
  }
  .site-subnav {
    flex: 1 0 100%;
    padding-inline-start: var(--sp-4);
    padding-block-end: var(--sp-2);
    border-inline-start: var(--border-hairline) solid var(--color-border-on-dark);
    margin-inline-start: var(--sp-1);
  }
  .site-subnav__link { font-size: var(--fs-300); }
  .site-nav__link {
    padding: var(--sp-3) 0;
    font-size: var(--fs-500);
    border-bottom: var(--border-hairline) solid var(--color-border-on-dark);
  }
  /* The sweeping rule is off in the drawer, so the current page was marked by
     recoloring the label to --color-accent, 4.70:1 on the ink. That made the one
     link the visitor most needs to place the weakest text in the drawer. It now
     carries the desktop's marker instead, white and bold against the muted rest,
     which reads as clearly and holds 16.0:1. */
  .site-nav__link::after { display: none; }
  .site-nav__link[aria-current='page'] {
    color: var(--color-text-on-dark);
    font-weight: var(--weight-bold);
  }
}

/* [MOBILE] Narrow-phone header budget.
   The row is logo 57.6 + gap 12 + CTA + gap 12 + toggle 44. Measured in the
   browser, the full "Request a Quote" label makes that row about 321px, which
   my earlier arithmetic underestimated. Against available width:
     414px -> 382  ok     375px -> 343  ok, 22px slack
     393px -> 361  ok     360px -> 328  only 7px
                          320px -> 288  overflows by 33px
   Two steps, smallest first. Below 24rem the header gutter tightens, which is
   enough down to about 369px. At or below 23rem the CTA label compresses to
   "Quote", taking the row to roughly 208px and leaving 80px of headroom at
   320px. 375px keeps the full label and is deliberately untouched. */
@media (max-width: 24rem) {
  .site-header__inner { --gutter: var(--sp-4); }
}

/* [BOTH] The CTA label compresses in two separate places, for two unrelated
   reasons, using one mechanism.

   Narrow phones, at or below 23rem, per the budget above.

   The desktop half of this rule is GONE, and the reason is worth recording so it
   is not reinstated by reflex. The bound was 68rem when the nav carried seven
   items, then 65rem after About was cut. Removing Private Label as well took out
   the widest remaining item, and a forced-long-label sweep now shows the full
   "Request a Quote" holding a single row all the way down to 960px, which is the
   bottom of the desktop nav range, with 163px of spare slack between the logo
   and the nav. There is no desktop width where the long label does not fit, so
   there is nothing left to compress for. Desktop always shows the full label.

   What remains is below 60rem, and it is a different job entirely: it is not
   about width. "Request a Quote" in the header repeated the hero's own button
   almost word for word on a phone, so the header shortens to "Quote" to remove
   the echo. If nav items are ever added back, re-measure before assuming this
   bound still covers the width case too, because it no longer does.

   The accessible name is unaffected either way. It lives in aria-label on the
   anchor, so it stays "Request a quote by email" at every width no matter which
   span is visible, and the short span is aria-hidden so it is never announced
   alongside it. */
@media (max-width: 59.999rem) {
  .site-header__cta-long { display: none; }
  .site-header__cta-short { display: inline; }
}

@media (min-width: 60rem) {
  .nav-toggle { display: none; }
  .site-header__inner { gap: var(--sp-6); }
  .site-header__brand { margin-inline-end: 0; }
  .site-nav { margin-inline-start: auto; }
  .site-header__cta {
    padding: var(--sp-3) var(--sp-5);
    font-size: var(--fs-300);
    min-height: var(--tap-min);
  }

  /* [DESKTOP] Nav rhythm. The list spaces its items with one thing only, the
     flex gap on .site-nav__list, and no item carries a horizontal margin. The
     Products item was still reading wider than the rest because the disclosure
     button is a 36px --tap-sm square wrapped around a 0.6em chevron, so about
     14px of empty button box sat between the glyph and the item's right edge
     and stacked on top of the 24px list gap. The chevron looked 38px from
     Capabilities while every other pair measured 24px.
     Sizing the button to its glyph puts the item's right edge on the chevron,
     so the only space left between items is the list gap.
     The 36px target is not lost, it is just not needed here: on desktop the
     entire item opens the panel on hover and on focus, and the button is the
     supplementary explicit control. In the drawer, where it is the only way in,
     it keeps the full --tap-sm box untouched. */
  .site-nav__item--has-sub { gap: var(--sp-1); }

  .site-nav__disclosure {
    width: auto;
    /* auto height plus stretch makes the hit area as tall as the item, which
       buys back vertical target size at zero cost to horizontal rhythm. */
    height: auto;
    align-self: stretch;
  }

  /* [DESKTOP] The subnav becomes a floating panel. This is the one place the
     component genuinely differs in kind rather than in layout: on mobile it is
     an indented list inside the drawer, fully specified at base, and only the
     panel chrome lives here. Mobile needs nothing from this block. */
  .site-subnav {
    position: absolute;
    top: 100%;
    left: calc(var(--sp-4) * -1);
    z-index: 60;
    min-width: 15rem;
    margin-block-start: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    /* Raised rather than --color-surface-dark, so the panel separates from the
       bar it drops out of. Verified on #1E2C27: rest 6.84:1, hover 12.77:1,
       accent wipe 3.74:1 against the 3:1 non-text threshold. */
    background: var(--color-surface-dark-raised);
    border: var(--border-hairline) solid var(--color-border-on-dark);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
  }

  /* A hover menu that closes the instant the pointer leaves the label would be
     unusable, since the gap between the link and the panel is dead space. The
     pseudo bridges it so travelling down to the panel never crosses a hole. */
  .site-nav__item--has-sub[data-open='true']::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    height: var(--sp-2);
  }
}

/* --------------------------------------------------------------------------
   6. MAIN + PAGE HEAD
   -------------------------------------------------------------------------- */
.site-main { flex: 1 0 auto; }

/* The gap between an interior page head and the block under it, owned by one
   value that both sides read.

   It used to be produced two different ways. Contact got it from the head's own
   padding-bottom because .contact has no top padding, while Capabilities and
   Gallery got the head's padding-bottom AND the next section's 96px top padding
   on top of it. Measured, that was 64px of clear space on Contact against 160px
   on the other two: the same intent, two mechanisms, and a 96px difference
   nobody had asked for.

   The gap is measured to the next section's BOX, and the head owns all of it.
   A section's own top padding is its own business, not part of this gap: on
   Contact the next section has none, and on Capabilities the next section is a
   full-bleed dark band whose internal padding has to stay symmetric with its
   bottom or the band looks top-loaded.

   An earlier version of this rule also zeroed the following section's top
   padding to force the CONTENT gap to match too. That was wrong: it flattened
   the stat band's 96px top against a 96px bottom and jammed the figure to the
   band's top edge. One value, applied in one place, on the head only. */
.page-head {
  padding-block: var(--sp-7) var(--page-head-gap);
}

.page-head__title {
  max-width: var(--container-narrow);
  font-size: var(--fs-800);
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-display);
}

/* Real scale gap between the H1 and the tier below it, not a few px. */
.page-head__lead {
  max-width: var(--measure);
  margin-block-start: var(--sp-5);
  font-size: var(--fs-600);
  line-height: 1.45;
  color: var(--color-text-muted);
  /* Many lines on a phone: pretty is the right tool, it protects the last line
     from a single stranded word. */
  text-wrap: pretty;
}

/* --- Centred page head. Products only; Contact shares .page-head and keeps its
   left-aligned setting, which is why this is a modifier rather than a change to
   the base. ------------------------------------------------------------- */
/* Products only. One step up from the shared --page-head-gap, and Products is
   already off that unification, so this is not a divergence to reconcile.

   What sits under this head is six solid saturated colour blocks, not the light
   content every other page puts below its intro. At --sp-6 the cards crowded the
   last line of the intro at 40px, which also read bottom-heavy because the head
   carries 64px ABOVE the title. --sp-7 gives the grid room and makes the head
   symmetric at 64 top and bottom.

   Applied at the base, not only at 60rem: the gap measured the same 40px at every
   width and the mobile cards are full-bleed, so if anything they were heavier.
   Nothing else about the mobile head moves: the 64px header-to-title, the 24px
   title-to-intro, the two-line title and the centring are all untouched. */
.page-head--center {
  text-align: center;
  padding-block-end: var(--sp-7);
}

/* 40rem, not --container-narrow. This is the whole fix for the stranded "One":
   "One standard." measures 607px at the 88px cap and --container-narrow is
   592px, so the phrase could not fit on one line and the browser broke it. A
   <br> would have solved the desktop and overflowed a 320px viewport by 31px,
   because at the --fs-800 floor the phrase is wider than that screen. Widening
   the measure lets the two-line break happen on its own from roughly 350px up,
   and below that the line wraps naturally instead of overflowing. */
.page-head--center .page-head__title {
  max-width: 40rem;
  margin-inline: auto;
}

/* balance at every width here, not just at 60rem as the base rule does. Centred
   text makes an uneven rag look accidental, and this sub is short enough that
   balance has an easy job. */
.page-head--center .page-head__lead {
  max-width: 44rem;
  margin-inline: auto;
  text-wrap: balance;
}

@media (min-width: 60rem) {
  :root { --page-head-gap: var(--sp-7); }
  .page-head { padding-block: var(--sp-8) var(--page-head-gap); }
  /* Tighter than the base head. A centred intro that hands off to the colour
     grid should read as a confident label for what follows, not as a section in
     its own right, so it gives back a step of vertical space at both ends. */
  .page-head--center { padding-block: var(--sp-7); }

  /* Two lines on a desktop: pretty has nothing to do, because a 26 character
     second line is a short line, not an orphaned word. balance evens the two
     out instead of letting the break land on the sentence boundary. It is
     scoped here because balance is only useful at low line counts. */
  .page-head__lead { text-wrap: balance; }
}

/* --------------------------------------------------------------------------
   7. CONTACT  [BOTH]
   Asymmetric split. Message anchored left at the narrow measure, elevated map
   panel right. align-items:start, never center, so a tall map cannot dump a
   void beside a shorter text column.
   -------------------------------------------------------------------------- */
.contact {
  padding-block-end: var(--sp-8);
}

.contact__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-7);
  align-items: start;
}

@media (min-width: 60rem) {
  .contact__grid {
    grid-template-columns: minmax(0, var(--col-primary)) minmax(0, 1fr);
    gap: var(--sp-8);
  }
}

.contact__primary {
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}

/* --- RFQ block, the one action ------------------------------------------- */
.rfq {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-4);
  padding: var(--sp-6);
  background: var(--color-surface);
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}

.rfq__title {
  font-size: var(--fs-700);
  letter-spacing: var(--tracking-tight);
}

.rfq__note {
  font-size: var(--fs-300);
  color: var(--color-text-muted);
  max-width: var(--measure);
}

.rfq .btn--accent { align-self: stretch; }

@media (min-width: 40rem) {
  .rfq .btn--accent { align-self: flex-start; }
}

/* --- Data plate. Address, phone, email. A definition list, not cards. ---- */
.plate {
  display: grid;
  gap: var(--sp-5);
  padding-block-start: var(--sp-2);
}

.plate__row {
  display: grid;
  gap: var(--sp-2);
  padding-block-end: var(--sp-5);
  border-bottom: var(--border-hairline) solid var(--color-border);
}
.plate__row:last-child {
  padding-block-end: 0;
  border-bottom: 0;
}

.plate__value {
  font-size: var(--fs-600);
  font-weight: var(--weight-medium);
  line-height: 1.35;
  letter-spacing: var(--tracking-tight);
}

/* The underline is always present and always transparent, so hover animates a
   color rather than snapping a rule into existence. It is the text's own
   decoration, not a border, which keeps it hugging the baseline at every font
   size instead of dropping to the bottom of the line box. */
.plate__value a {
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: var(--underline-thickness);
  text-underline-offset: var(--underline-offset);
  transition:
    color var(--dur-fast) var(--ease-standard),
    text-decoration-color var(--dur-fast) var(--ease-standard);
}
.plate__value a:hover {
  color: var(--color-accent-deep);
  text-decoration-color: currentColor;
}

.plate__meta {
  font-size: var(--fs-200);
  color: var(--color-text-muted);
}

/* [MOBILE] The plate steps down as one block so it stays the same object, just
   smaller: the value drops from --fs-600 to the measured --fs-plate-value, and
   the helper lines drop one tier from --fs-200 to --fs-100. The dt labels are
   deliberately not in here. They already sit at --fs-100, which is the site's
   body floor, so a "uniform" step would have pushed them through it. Holding
   them still is what keeps the block above the floor rather than a rounding
   accident.
   Only the two linked values get nowrap, and .plate__value a is exactly those
   two: the address has no anchor, so it keeps wrapping across its <br> as
   before, and so do every label and helper line.
   This block sits after the base rules on purpose. A media query adds no
   specificity, so at equal specificity the later rule wins and hoisting this
   above .plate__meta would silently disable the step down. */
@media (max-width: 39.999rem) {
  .plate__value { font-size: var(--fs-plate-value); }
  .plate__meta { font-size: var(--fs-100); }
  .plate__value a { white-space: nowrap; }
}

/* --- Map panel. Elevated card. Click to load, so nothing third party is
   requested until the visitor asks for it. ------------------------------- */
.map {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--color-surface);
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

.map__figure {
  position: relative;
  margin: 0;
  aspect-ratio: 4 / 3;
  background: var(--color-surface-sunken);
}

@media (min-width: 60rem) {
  .map__figure { aspect-ratio: 1 / 1; }
}

.map__svg,
.map__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* The whole schematic is the target, so the hit area is the panel not a pill */
.map__load {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: var(--sp-5);
  background: transparent;
  border: 0;
  cursor: pointer;
  transition: background-color var(--dur-base) var(--ease-standard);
}
@media (hover: hover) and (pointer: fine) {
  .map__load:hover { background: rgb(var(--shadow-rgb) / 0.06); }
}

.map__load-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap-min);
  padding: var(--sp-2) var(--sp-5);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: var(--fs-200);
  font-weight: var(--weight-bold);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-md);
  transition:
    transform var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .map__load:hover .map__load-pill {
    transform: translateY(var(--lift-md));
    box-shadow: var(--shadow-lg);
  }
}
.map__load:active .map__load-pill { transform: scale(var(--press-scale)); }

/* Sized in em so it tracks the pill's label rather than needing its own scale.
   currentColor, so it is the label's sibling and not a second accent competing
   with the orange pin already sitting in the schematic behind it. */
.map__load-icon {
  flex: 0 0 auto;
  width: 1.05em;
  height: 1.05em;
}

/* One child, so the two-item flex machinery is gone: nothing to space apart,
   wrap, or baseline-align. A padded rule under the schematic holding a single
   link out. */
.map__footer {
  padding: var(--sp-4) var(--sp-5);
  border-top: var(--border-hairline) solid var(--color-border);
  background: var(--color-surface);
}

/* Same inline-link recipe as the contact plate and the footer contact block, so
   the page has one hover language instead of three. The at-rest color stays
   --color-accent-deep rather than starting neutral: this is a standalone call
   out to Google Maps with no surrounding sentence to mark it as a link, so it
   has to look like one before it is hovered. 5.84:1 on the white footer. */
.map__out {
  font-size: var(--fs-200);
  font-weight: var(--weight-bold);
  color: var(--color-accent-deep);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: var(--underline-thickness);
  text-underline-offset: var(--underline-offset);
  transition: text-decoration-color var(--dur-fast) var(--ease-standard);
}
.map__out:hover { text-decoration-color: currentColor; }

/* Inline SVG schematic, colored from the tokens. Land sits darker than the road
   grid so the roads read as roads. Major and minor differ by width, which is
   how a real locator distinguishes them. */
.map__ground { fill: var(--grey-100); }
.map__road { stroke: var(--white); }
.map__road--major { stroke: var(--white); }
.map__pin-ring { fill: rgb(225 81 38 / 0.18); }
.map__pin { fill: var(--color-accent); }
/* Charcoal, not black: --color-text-muted is #4A4E4A, which measures 8.47:1 on
   the white road and sits in the same warm grey family as the rest of the map
   instead of punching a hole in it.
   Centering lives on the element as presentation attributes next to
   text-anchor, since older Safari honors the attribute but not the CSS
   property. */
.map__road-label {
  fill: var(--color-text-muted);
  font-family: var(--font-sans);
  font-size: var(--map-label-fs);
  font-weight: var(--weight-bold);
  letter-spacing: 0.1em;
}

/* --------------------------------------------------------------------------
   8. PRODUCTS  [BOTH]
   The signature page. Contact spends its whole palette on ink plus one orange
   action; this is where the five withheld brand colors finally deploy, so the
   color is allowed to be loud here in a way it is nowhere else on the site.
   THE MOVE: the tile IS the color. The clickable target is the fluid color
   itself, edge to edge, with the jug standing on it. Not a bottle thumbnail
   with a color caption underneath.
   -------------------------------------------------------------------------- */
.families {
  padding-block-end: var(--sp-8);
}

.family-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-5);
}

@media (min-width: 40rem) {
  .family-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 60rem) {
  .family-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-6);
  }
}

/* The six colour pairs used to be restated here as .family--* modifiers. They
   now come from the [data-family] block in section 2b, which the detail pages
   and the nav dropdown read from the same source. PurpleZone's label art is
   pink rather than violet, which is why that family maps to --brand-pink. */

/* Rows, not align-items:center. The jug takes the flexible row and the label
   sits in an auto row pinned to the bottom, so a shorter descriptor never
   floats in a pool of dead space against a taller neighbour.
   (KICKOFF PART 1 #7, the recurring centre-void bug.) */
.family {
  position: relative;
  display: grid;
  /* minmax(0, 1fr), not 1fr. A bare 1fr is minmax(auto, 1fr), so the jug's
     intrinsic 1200px min-content width would fight the fixed aspect-ratio and
     push the tile taller than it should be. Zero as the floor lets the row
     shrink to whatever the ratio leaves it. */
  grid-template-rows: minmax(0, 1fr) auto;
  aspect-ratio: 4 / 5;
  padding: var(--sp-5);
  background: var(--fam-field);
  color: var(--fam-on);
  text-decoration: none;
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition:
    transform var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out);
}

/* --color-focus is brand orange, which is invisible on the orange tile. The
   tile's own ink is the only ring guaranteed to be visible on all six, and it
   inherits the measured ratio above. */
.family:focus-visible {
  outline: var(--border-thick) solid var(--fam-on);
  /* Inset, because the tile clips its children and an outside ring on a
     full-bleed color field reads as a gap rather than as focus. */
  outline-offset: calc(var(--focus-offset) * -1 - 1px);
  /* The base :focus-visible rule forces --radius-sm on anything focused, which
     would snap this tile's corners from 0.875rem to 0.25rem the moment it takes
     focus. Restating the tile's own radius holds the shape still. */
  border-radius: var(--radius-lg);
}

/* The cutout carries roughly a fifth of transparent margin on each side of the
   bottle, so 100% here renders a jug about 55% of the tile's height. Present,
   but not competing with the color field it stands on. */
.family__jug {
  place-self: center;
  width: auto;
  max-width: 100%;
  max-height: 100%;
  filter: var(--shadow-jug);
  transition: transform var(--dur-base) var(--ease-out);
}

.family__label {
  display: grid;
  gap: var(--sp-1);
}

.family__name {
  font-family: var(--font-heading);
  font-size: var(--fs-600);
  font-weight: var(--weight-heading);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-tight);
}

/* Full --fam-on, never a faded version of it. The hierarchy against the name
   is carried by size and weight alone. Dropping this to 70% opacity would push
   the red tile from 4.61:1 to roughly 3.2:1 and quietly fail the whole page. */
/* Two lines are reserved whether or not the copy needs them. YellowZone's
   descriptor is one line and every other family's is two, so without a floor
   its name and rule sit a line lower than its row-mates and the whole row looks
   misaligned. Reserving the space costs an empty line on one tile and buys a
   shared baseline across all six. Expressed from the line-height token, so it
   tracks if the type scale moves.
   min-height, not height: a width where something wraps to three lines still
   grows rather than clipping. */
.family__desc {
  min-height: calc(var(--lh-body) * 2em);
  font-size: var(--fs-200);
  font-weight: var(--weight-body);
  line-height: var(--lh-body);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: var(--underline-thickness);
  text-underline-offset: var(--underline-offset);
  transition: text-decoration-color var(--dur-fast) var(--ease-standard);
}

/* Formula count. Same ink, held apart by tracking and case rather than by a
   lighter color, for the same contrast reason as the descriptor. */
.family__meta {
  position: absolute;
  top: var(--sp-5);
  right: var(--sp-5);
  font-size: var(--fs-100);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}

/* A tile is a card, so it takes the card language already used by the primary
   button and the map pill: lift plus elevation. The descriptor's underline is
   the one link dialect adapted to the tile, drawn in --fam-on because orange
   would disappear on the orange field. */
@media (hover: hover) and (pointer: fine) {
  .family:hover {
    transform: translateY(var(--lift-sm));
    box-shadow: var(--shadow-lg);
  }
  .family:hover .family__jug { transform: scale(1.03); }
  .family:hover .family__desc { text-decoration-color: currentColor; }
}

.family:active { transform: scale(var(--press-scale)); }

/* --- Line card. The calm tier after the loud grid. All seven SKUs stated as
   plain data, which is also where the seventh becomes visible: the grid has six
   tiles because GreenZone is one family with two formulas. ------------------ */
.line-card {
  padding-block-end: var(--sp-8);
}

.line-card__title {
  font-size: var(--fs-100);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-block-end: var(--sp-4);
}

/* [MOBILE] Each SKU is its own surfaced block. A three-column table has nowhere
   to go at 320px, and stacking the columns with only a hairline between rows
   makes the name, the use, and the temperature read as one paragraph. Giving
   each SKU a real card with its own border means the eye can find where one
   formula ends and the next begins without reading a word. */
.line-card__list {
  display: grid;
  gap: var(--sp-3);
}

/* Each row is a link to its family page, which is what earns the rise on hover.
   Both GreenZone rows point at the same page, since one page carries both
   formulas. The accessible name is the row's own text plus a visually hidden
   "product details", so it stays complete and distinct per row and still
   contains every visible word, which an aria-label summarising the row would
   not (WCAG 2.5.3, and voice control needs the visible words to match). */
.line-card__row {
  position: relative;
  display: grid;
  gap: var(--sp-2);
  padding: var(--sp-4);
  color: var(--color-text);
  text-decoration: none;
  background: var(--color-surface);
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--radius-md);
  transition:
    transform var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out);
}

/* The base :focus-visible forces --radius-sm, which would square off a row that
   is rounded at rest on mobile and rounds on hover at desktop. */
.line-card__row:focus-visible {
  border-radius: var(--radius-md);
}

/* [DESKTOP] Above the small breakpoint there is room for the real three-column
   read, so the cards flatten back into a data table: no surface, no radius, one
   hairline between rows. The negative inline margin plus matching padding keeps
   the text aligned to the container edge while leaving the hover surface room
   to breathe past it. */
@media (min-width: 40rem) {
  .line-card__list {
    gap: 0;
    margin-inline: calc(var(--sp-3) * -1);
    border-top: var(--border-hairline) solid var(--color-border);
  }

  .line-card__row {
    grid-template-columns: minmax(0, 14rem) minmax(0, 1fr) auto;
    gap: var(--sp-4);
    align-items: baseline;
    padding: var(--sp-4) var(--sp-3);
    background: none;
    border: 0;
    border-bottom: var(--border-hairline) solid var(--color-border);
    border-radius: 0;
  }
}

/* The list is reference data, not a set of links, so the response is a quiet
   surface lift rather than the tile's full card treatment. Gated like every
   other lift on the site so a touch tap cannot strand a row raised. */
@media (hover: hover) and (pointer: fine) {
  .line-card__row:hover {
    transform: translateY(var(--lift-sm));
    background: var(--color-surface);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
    /* Lifts above its neighbours so the shadow is not painted over by the next
       row in source order. */
    z-index: 1;
  }
}

/* Dividers around a lifted row.
   In the flattened table form the rules between rows are drawn by each row's
   own border-bottom, plus the list's border-top above the first row. A lifted
   row rises 2px and rounds its corners, so TWO lines can show against it: its
   own, which would cut across the card, and the one directly above it, whose
   straight ends stay visible in the gap left by the new rounded top corners.
   That second line is the segment that protrudes past the raised card, and it
   belongs to the PREVIOUS row, not the hovered one, which is why hiding only
   the hovered row's border does not clear it.
   All three positions are covered: a middle or last row hides its own border
   and the previous row's, and the first row hides its own and the list's top
   border instead, since it has no previous sibling.
   Colors fade rather than the borders being removed, so nothing reflows. If
   :has() is unsupported the rules are simply dropped and the artifact returns,
   which is the same state as before this fix, never a worse one. */
@media (hover: hover) and (pointer: fine) and (min-width: 40rem) {
  .line-card__list,
  .line-card__row {
    transition:
      transform var(--dur-base) var(--ease-out),
      box-shadow var(--dur-base) var(--ease-out),
      border-color var(--dur-fast) var(--ease-standard);
  }

  .line-card__row:hover,
  .line-card__row:has(+ .line-card__row:hover) {
    border-bottom-color: transparent;
  }

  .line-card__list:has(> .line-card__row:first-child:hover) {
    border-top-color: transparent;
  }
}

.line-card__sku {
  font-size: var(--fs-300);
  font-weight: var(--weight-bold);
}

.line-card__use {
  font-size: var(--fs-200);
  line-height: var(--lh-body);
  color: var(--color-text-muted);
}

/* Tabular figures so the temperature column lines up as a column of numbers
   rather than drifting with the glyph widths. Full --color-text, not the muted
   grey: on mobile this is the row's payload, not a footnote to the use line. */
.line-card__temp {
  font-size: var(--fs-200);
  font-weight: var(--weight-bold);
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   9. PRODUCT DETAIL  [BOTH]
   The family template. GreenZone is the two-formula case and every other
   family is the same markup with one formula, so nothing here is written
   twice or branches on a count.
   Colour comes from [data-family] on <main>, never from a per-page rule, and
   it is a field only. See the rule in section 2b.
   -------------------------------------------------------------------------- */
.detail-back {
  padding-block: var(--sp-5) 0;
}

.detail-back__link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-200);
  font-weight: var(--weight-bold);
  color: var(--color-text-muted);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: var(--underline-thickness);
  text-underline-offset: var(--underline-offset);
  transition:
    color var(--dur-fast) var(--ease-standard),
    text-decoration-color var(--dur-fast) var(--ease-standard);
}
.detail-back__link:hover {
  color: var(--color-text);
  text-decoration-color: currentColor;
}

/* --- Hero. The one move: large format product on its own colour field. ---- */
.detail-hero {
  margin-block-start: var(--sp-5);
  padding: var(--sp-7) 0;
  background: var(--fam-field);
  color: var(--fam-on);
}

/* stretch, not center. This is what lets the staging panel match the text block:
   the row height is set by the taller column, which is always the text, and the
   panel then fills it. Centring left the panel sized to its own contents and the
   two sides visibly unequal. The text column is unaffected, since it is the one
   setting the height. */
.detail-hero__inner {
  display: grid;
  gap: var(--sp-6);
  align-items: stretch;
}

.detail-hero__eyebrow {
  font-size: var(--fs-100);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  /* Full --fam-on, not a faded one. On the yellow field a softened eyebrow
     would be the first thing to fall under the floor. */
  color: var(--fam-on);
  margin-block-end: var(--sp-3);
}

.detail-hero__title {
  font-size: var(--fs-800);
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-display);
}

/* The bottom margin is the gap above the button, so it is half of the button's
   optical centring. Its partner is the jump row's margin-top. */
.detail-hero__lead {
  margin-block: var(--sp-4) var(--sp-5);
  font-size: var(--fs-500);
  line-height: var(--lh-body);
  max-width: 40ch;
}

/* [DESKTOP] Four more pixels above the button, and only above 40rem.
   The button has to look centred between this paragraph and the jump row, which
   is not the same as equal margins, because the shadow sits below the button and
   swallows the first 16px underneath it while the paragraph contributes 3.84px of
   descender and leading above it.
   The reason this is breakpoint scoped rather than global is the jump row. At
   40rem it turns from a column into a row, and its first ink moves from 6.64px
   below the row's top edge to 11.11px, because the row then centres its label
   against 32px tall links. That 4.5px shift is the whole discrepancy:
     below 40rem, --sp-5 already balances to within 1.20px
     at and above 40rem, --sp-5 leaves the lower gap 3.27px looser, which is what
     reads as the button sitting high
   Adding --sp-1 here takes the desktop case to 0.73px. Applying it at every width
   would have pushed mobile to 5.20px out in the other direction, making the
   button read low on phones to fix a desktop problem. */
@media (min-width: 40rem) {
  .detail-hero__lead {
    margin-block-end: calc(var(--sp-5) + var(--sp-1));
  }
}

/* The slot takes one jug or two without a count-specific rule, which is what
   lets the five single formula families clone this file untouched. */
/* It is also the staging panel. No wrapper element was added: this element was
   already the jug container on all six pages, so making it the panel is a pure
   stylesheet change and the six HTML files stay untouched.

   height:100% is what makes the panel self-correcting. The row height is set by
   the text column, which differs per page (419px on GreenZone and BlueZone, 388px
   on the others at 1440), so the panel matches the text block exactly on every
   page with no per-page value and nothing hardcoded. Capped at 26rem so it stays
   roughly square: about 419x416 on GreenZone, 388x416 on YellowZone. */
.detail-hero__jugs {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4);
  height: 100%;
  /* width + justify-self, NOT margin-inline:auto. An auto inline margin overrides
     the grid's inline stretch, so the panel shrink-wrapped to its contents: 336px
     on the single-jug pages against 416px on GreenZone, which is exactly the
     lopsidedness this was meant to remove. */
  width: 100%;
  max-width: 26rem;
  justify-self: center;
  padding: var(--sp-5);
  background: var(--fam-panel);
  border-radius: var(--radius-md);
  /* currentColor is --fam-on, set on .detail-hero, so the edge is the family's own
     measured foreground at low alpha and needs no second colour. Against the panel
     it reads 1.220 on red through 1.712 on green. */
  box-shadow:
    inset 0 0 0 var(--border-hairline)
    color-mix(in srgb, currentColor 18%, transparent);
}

.detail-hero__fig {
  flex: 1 1 0;
  min-width: 0;
  max-width: 15rem;
  margin: 0;
  display: grid;
  justify-items: center;
  gap: var(--sp-2);
}

/* The contact shadow. Both the ellipse and the jug sit in the SAME grid cell, so
   the ellipse's bottom edge IS the jug's base by construction. Nothing has to be
   derived from the caption's height and nothing needs re-tuning if that caption
   ever wraps.

   Plain black at low alpha rather than a fixed ink. Scaling a colour toward black
   darkens it proportionally, so this reads on every family including the darkest,
   which is exactly where --shadow-jug fails: that fixed ink measures 1.56:1 on
   GreenZone, the same failure mode as the Home hero where it measured 1.103:1. */
.detail-hero__fig::before {
  content: '';
  grid-row: 1;
  grid-column: 1;
  align-self: end;
  justify-self: center;

  /* Wider than the bottle, so it reads as a pool the jug stands in rather than a
     band tucked under it. The bottle occupies 50.1% to 57.1% of its canvas across
     the seven images, mean 54%, so 74% clears it by about 10% on each side. */
  width: 74%;
  /* A floor oval, not a strip. The old 84% x 1.6em worked out at roughly 9:1,
     which is a bar however softly it is filled. */
  aspect-ratio: 3.4 / 1;

  /* THE ACTUAL BUG, and it was position rather than colour. These cutouts carry
     8.08% to 9.92% of empty canvas below the bottle, mean 9.24%, which at a 288px
     image is 23 to 29px. The ellipse was aligned to the IMAGE box, so it sat that
     far below the feet: a detached smudge with clear ground between it and the
     jug, which is exactly what reads as a dark bar.
     A percentage block margin resolves against the containing block's WIDTH, and
     these canvases are square, so 9.24% of the width is 9.24% of the height. That
     lifts the ellipse's bottom edge onto the bottle's base line. translateY then
     centres it ON that line, so the density sits at the contact point.
     One shared value, not seven: the spread is 1.84% of image height, about 5px
     at this size, which is under the blur radius and invisible. */
  margin-block-end: 9.24%;
  transform: translateY(50%);

  /* Four stops reaching zero only at the very edge, so it feathers the whole way
     out instead of ending at 72% with an edge to see. Densest at the contact
     point and low to begin with: the previous 0.24 with a short falloff is what
     made it a smudge.
     Plain black at low alpha is deliberate, not a shortcut. It scales all three
     channels equally, so the hue is held EXACTLY and chroma still measures 83 to
     85% of the panel's: a yellow floor in shadow stays a darker yellow rather
     than going grey. It also needs no @supports fallback, unlike the panel. */
  background: radial-gradient(
    closest-side ellipse,
    rgba(0, 0, 0, 0.20) 0%,
    rgba(0, 0, 0, 0.13) 38%,
    rgba(0, 0, 0, 0.05) 70%,
    rgba(0, 0, 0, 0) 100%
  );
  pointer-events: none;
}

/* Placed explicitly into the same cell as the ellipse. Without this the ellipse
   would take row 1 on its own and push the jug down a row. */
.detail-hero__jug {
  grid-row: 1;
  grid-column: 1;
  width: 100%;
  height: auto;
  /* Kept. This gives the bottle its own form; the ellipse grounds it to a
     surface. Two different jobs, and this one still reads on five of six. */
  filter: var(--shadow-jug);
}

/* Full --fam-on. On the yellow field this is the caption that would fail first
   if it were softened, and it is naming a product, not whispering. */
.detail-hero__cap {
  font-size: var(--fs-100);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fam-on);
  text-align: center;
}

/* --- Jump row. In-page anchor nav, sitting on the hero's colour field. -----
   [BOTH] Same mechanic as every other link on the site: a rule that wipes in
   from the left over --dur-base on --ease-out, with the label holding still.

   The rule is currentColor, NOT --color-accent, and that is a correctness fix
   rather than a preference. Measured against the six family fields the orange
   rule scores: green 2.73, orange 1.00, red 1.19, blue 1.66, purple 1.41,
   yellow 3.04. Five of six sit under the 3:1 non-text threshold and on the
   OrangeZone field the rule is the same colour as the field it is drawn on, so
   it is literally invisible. currentColor resolves to --fam-on, which is by
   definition the foreground already proven against that field, so the pattern
   is correct on all six the day it is cloned.
   Same reasoning for the focus ring: --color-focus is the brand orange and
   would be a 2.73:1 ring on green.
   No new colour enters the system either way. ------------------------------ */
/* [MOBILE] A vertical stack: label on its own line, then one link per line.
   Wrapping a row was the wrong answer at this width. The label plus the first
   link plus the bullet measure about 249px against 327px of container, so the
   bullet fits on line one and only the second link wraps, which strands the
   bullet at the end of the first line pointing at nothing.
   align-items: flex-start matters. These links are flex children, so the
   default stretch would run them the full container width and the hover rule,
   which is width: 100% of the link, would stretch with them. */
/* [BOTH] The button should look centred between the lead above it and this row
   below it, which is not the same as equal margins. Two things bias it:
     The lead ends in text, so 3.84px of descender and leading sit between its
     lowest ink and its block bottom. Its --sp-5 margin therefore reads as
     27.84px of clear space above the button box.
     The button ends in a shadow. --shadow-accent is offset downward only and
     occupies about 16px below the box, so the first 16px of whatever margin
     goes here is not clear space at all.
   Solving for equal clear space wants 32.7px on desktop, where the row centres
   its label against 32px tall links, and 37.2px on mobile, where the row is a
   column and its label starts at the top. 36px sits between them: the lower gap
   reads 3.3px looser than the upper on desktop and 1.2px tighter on mobile,
   both under the threshold where an eye reads them as different.
   Composed from --sp-5 plus --sp-3 rather than jumping to --sp-6, because the
   scale steps 24 to 40 and 40px overshoots by 7.3px on desktop.
   The space is on this row, never on the button, which is shared. */
.jump {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-2);
  margin-block-start: calc(var(--sp-5) + var(--sp-3));
  /* Set once here so the label, the links, the arrows, and the separator all
     inherit it, which is what makes the component theme agnostic. */
  color: var(--fam-on);
}

.jump__label {
  font-size: var(--fs-100);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}

/* [MOBILE] --tap-min is the site's own 44px target token, the same one the
   buttons and the map pill use. Stacked links need it; in the desktop row the
   links sit inside running text and it is removed again. */
.jump__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap-min);
  padding-block: var(--sp-1);
  font-size: var(--fs-200);
  font-weight: var(--weight-medium);
  color: inherit;
  text-decoration: none;
}

.jump__link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: var(--border-thick);
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease-out);
}

.jump__link:focus-visible {
  outline-color: var(--fam-on);
  border-radius: var(--radius-sm);
}

/* Borders on two sides plus a 45 degree turn is the chevron the header already
   uses, at hairline weight because this one sits inside running text. */
.jump__arrow {
  width: 0.45em;
  height: 0.45em;
  border-right: var(--border-hairline) solid currentColor;
  border-bottom: var(--border-hairline) solid currentColor;
  transform: translateY(-20%) rotate(45deg);
  transition: transform var(--dur-base) var(--ease-out);
}

/* A dot, not a slash, so the separator carries no character a screen reader
   could reach even if the aria-hidden were ever dropped.
   [MOBILE] Out of the layout entirely rather than merely invisible: it divides
   two things sitting side by side, and once they are stacked there is nothing
   for it to divide. display: none also keeps it from occupying a row of its
   own in the column. */
.jump__sep {
  display: none;
  width: var(--sp-1);
  height: var(--sp-1);
  background: currentColor;
  border-radius: 50%;
  opacity: 0.45;
}

/* [BOTH] From the small breakpoint up the row fits and returns unchanged. The
   whole row measures about 412px and the container is 592px at 40rem, so there
   is 180px of slack and none of the mobile wrap problem exists here. Restoring
   at 40rem rather than at the desktop breakpoint keeps tablet identical to what
   it was, since the row was never broken at that width.
   Layout only: direction, wrap, alignment, gap, the target floor, and putting
   the separator back. Nothing visual changes. */
@media (min-width: 40rem) {
  .jump {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2) var(--sp-4);
  }
  .jump__link { min-height: 0; }
  .jump__sep { display: block; }
}

@media (hover: hover) and (pointer: fine) {
  .jump__link:hover::after { transform: scaleX(1); }
  /* The arrow nudges toward the section it points at. It keeps the 45 degree
     rotation, because that rotation is the arrow rather than an animation. */
  .jump__link:hover .jump__arrow { transform: translateY(15%) rotate(45deg); }
}

/* --- Formulas. The comparison, and only the axes that differ. ------------- */
.detail-section {
  padding-block: var(--sp-7);
}

/* [BOTH] Any detail section that is an anchor target clears the sticky header,
   which is 4.5rem tall and would otherwise cover the top of whatever the link
   just jumped to. Keyed off [id] rather than a class so a section becomes
   anchor-safe simply by being given an id, which is what keeps this correct
   when the five remaining families are cloned. */
.detail-section[id] {
  scroll-margin-top: var(--header-h);
}

.detail-section__title {
  font-size: var(--fs-700);
  margin-block-end: var(--sp-2);
}

.detail-section__note {
  font-size: var(--fs-300);
  color: var(--color-text-muted);
  max-width: var(--measure);
  margin-block-end: var(--sp-6);
  /* [BOTH] Orphan control that is copy agnostic, which is the whole requirement:
     six families clone this and their sentences differ in length, so anything
     tuned to one family's copy would land wrong on the other five.
     Measured on the greedy wrapping that browsers do by default, single word last
     lines appear in narrow width windows that spot checks miss. On the two built
     pages: 428 to 456px and 428 to 484px for the formula note, 324 to 368px and
     408 to 460px for the documents notes. Those windows contain real devices, 360
     for common Android, 414 for an older Plus, 430 for a Pro Max.
     balance first, pretty second. A browser with both takes pretty, which is the
     correct one for running text; a browser with only balance still evens the two
     lines, which also removes the orphan; a browser with neither drops both and
     wraps greedily as before. No max-width, no forced break. */
  text-wrap: balance;
  text-wrap: pretty;
}

/* A note that holds two sentences is ONE paragraph with the second sentence in a
   span, not two paragraphs.
   [MOBILE] base is inline, so the note flows as a single cohesive block with the
   sentences separated by nothing more than the space between them. Two <p> tags
   for two short sentences read as fragmented micro-paragraphs on a narrow column,
   which is what this replaces.
   [DESKTOP] the span becomes a block at 60rem, the site's existing large
   breakpoint and the same one the nav row and the hero split already use, so the
   second sentence starts a fresh line where there is room for it to land as its
   own thought. A block carries no default margin, so the new line arrives at
   normal line spacing with no gap added.
   Copy agnostic by construction: whatever a family's second sentence is, it goes
   in the span and behaves identically. No max-width, no <br>, no forced break,
   nothing tuned to one family's sentence length.
   This replaced a :not(:last-of-type) sibling-margin pair, which is now removed
   rather than left inert: with every note a single paragraph the selector could
   never match, and leaving it would have been a trap, since a future two-paragraph
   note would silently take the --sp-6 section gap between its sentences. */
.note__desktop-block {
  display: inline;
}

@media (min-width: 60rem) {
  .note__desktop-block {
    display: block;
  }
}

/* auto-fit, so one formula fills the row as a single wide card and two sit
   side by side. No count class, no per page override. */
.formula-grid {
  display: grid;
  gap: var(--sp-5);
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 52rem) {
  .formula-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
    gap: var(--sp-6);
  }
}

/* [BOTH] The cap is on the ITEM, deliberately not on the grid track, and that
   distinction is load bearing. .formula-grid is repeat(auto-fit, minmax(min(100%,
   18rem), 1fr)); auto-fit decides how many tracks to create from the track's max
   sizing function, so capping the track would cut the repetition count to one and
   drop a two-formula family's second card onto its own row. Capping the item
   leaves the track count untouched.
   What it fixes: with one card the empty tracks collapse and the lone card takes
   the whole row, 1120px at desktop, where its three short data rows fill only 36%
   of it. That is squarely inside KICKOFF PART 1 #1's dead-space flag. At 34rem the
   card is 544px and fills about 77%.
   It is inert wherever it is not needed. A two-formula family's tracks resolve to
   540px, under the cap, so nothing moves. At mobile the grid is a single
   minmax(0, 1fr) column and the container is far narrower than 34rem, so the card
   is full width as before.
   The card left-anchors rather than centring: justify-self stays at its default
   stretch, a definite max-width means it cannot stretch, and stretch's fallback
   alignment is start. That matches the hero's left-anchored composition. */
.formula {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  max-width: 34rem;
  padding: var(--sp-5);
  background: var(--color-surface);
  border: var(--border-hairline) solid var(--color-border);
  /* The family colour returns as a rule along the top edge, tying the card to
     the hero without tinting any text. */
  border-block-start: var(--border-thick) solid var(--fam-field);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

.formula__name {
  font-size: var(--fs-600);
  letter-spacing: var(--tracking-tight);
}

.formula__rows {
  display: grid;
  gap: var(--sp-4);
}

.formula__value {
  font-size: var(--fs-400);
  line-height: 1.4;
}

.formula__temp {
  font-size: var(--fs-600);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-tight);
}

/* --- Shared spec. Stated once so neither formula card repeats the other. -- */
.spec-list {
  display: grid;
  gap: var(--sp-3);
  padding: 0;
  margin-block-start: var(--sp-4);
}

.spec-list__item {
  position: relative;
  padding-inline-start: var(--sp-5);
  font-size: var(--fs-300);
  line-height: var(--lh-body);
}

/* A rule, not a bullet glyph, so the marker sits on the type baseline grid and
   inherits the family colour without becoming a coloured character. */
.spec-list__item::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.62em;
  width: var(--sp-3);
  height: var(--border-thick);
  background: var(--fam-field);
}

/* Stacked label-plus-list groups. The gap lives on the adjacent sibling rather
   than on every block, so the first one never adds a stray leading space. */
.spec-block + .spec-block {
  margin-block-start: var(--sp-6);
}

.spec-grid {
  display: grid;
  gap: var(--sp-5);
  margin-block-start: var(--sp-6);
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 52rem) {
  .spec-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.capacity {
  width: 100%;
  border-collapse: collapse;
  margin-block-start: var(--sp-4);
  font-size: var(--fs-200);
  font-variant-numeric: tabular-nums;
}

.capacity th,
.capacity td {
  padding: var(--sp-3) 0;
  text-align: start;
  border-bottom: var(--border-hairline) solid var(--color-border);
}

.capacity th {
  font-size: var(--fs-100);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* --- Documents. ----------------------------------------------------------- */
.doc-list {
  display: grid;
  gap: var(--sp-3);
  margin-block-start: var(--sp-4);
}

/* [MOBILE] Stacked: the icon and name hold the first line, the label drops to its
   own line beneath them, left aligned. Measured at 375px the card gives 293px of
   inner width and the name column 261px, and five of the eight document rows in
   the line do not fit their name and label side by side there, so both columns
   wrap and fight each other. The worst is BlueZone's held-state row, where a
   231px name and a 196px label want 439px of a 261px line.
   The row stays one anchor and one tap target; stacking only makes it taller.
   [DESKTOP] returns to side by side at 60rem, the same breakpoint the notes use. */
.doc {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-4);
  background: var(--color-surface);
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text);
  text-decoration: none;
  transition:
    transform var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out);
}

.doc__icon {
  flex: 0 0 auto;
  width: 1.15em;
  height: 1.15em;
  color: var(--color-text-muted);
}

.doc__name { font-weight: var(--weight-bold); }

/* [MOBILE] A 100% flex-basis cannot share a line, so the label always lands on its
   own row, and margin-inline-start returns to 0 so it sits left rather than being
   pushed to the right edge of that new line. */
.doc__meta {
  flex: 0 0 100%;
  margin-inline-start: 0;
  font-size: var(--fs-100);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* [DESKTOP] Side by side again: the label sizes to its content and the auto margin
   pushes it to the right edge of the single line. */
@media (min-width: 60rem) {
  .doc {
    flex-wrap: nowrap;
  }

  .doc__meta {
    flex: 0 1 auto;
    margin-inline-start: auto;
  }
}

@media (hover: hover) and (pointer: fine) {
  .doc:hover {
    transform: translateY(var(--lift-sm));
    box-shadow: var(--shadow-md);
  }
}

/* --- Closing CTA band. Orange stays the action colour on every family. ---- */
.detail-cta {
  padding-block: var(--sp-7);
  border-top: var(--border-hairline) solid var(--color-border);
}

.detail-cta__title {
  font-size: var(--fs-700);
  margin-block-end: var(--sp-3);
}

.detail-cta__note {
  font-size: var(--fs-300);
  color: var(--color-text-muted);
  max-width: var(--measure);
  margin-block-end: var(--sp-5);
}

/* [DESKTOP] Asymmetric split, the copy anchored left against the product. */
@media (min-width: 60rem) {
  .detail-hero { padding: var(--sp-8) 0; }
  .detail-hero__inner {
    grid-template-columns: minmax(0, var(--col-primary)) minmax(0, 1fr);
    gap: var(--sp-8);
  }
  .detail-hero__fig { max-width: 18rem; }
}

/* --------------------------------------------------------------------------
   10. HOME  [BOTH]
   The page withholds every family color until the spectrum band, so sections
   1 and 2 are strictly ink and orange. The ground ladder carries the rhythm:
   off-white hero, white company band, the six fields themselves, off-white
   again, ink-deep capability, the sign, then off-white to close. The closing
   band is light on purpose, because the footer is already ink-deep and a dark
   close would merge into it.
   -------------------------------------------------------------------------- */

/* --- 1. Hero. A dark stage carrying the whole product line at scale.

   The ground is --ink-deep for a measured reason, not for mood. All six
   cutouts are WHITE bottles, so on the off-white ground they would repeat the
   YellowZone field-versus-silhouette collapse six times over. Composited on
   this ground the bottle bodies measure 13.94:1 to 17.87:1, and no jug has a
   dark pixel within 2px of its silhouette, so nothing sinks into the field.

   Loud through scale, contrast and choreography. Not through clutter. ----- */
.hero {
  position: relative;
  /* svh, not vh: on mobile browsers vh is the LARGER viewport and the address
     bar then crops the bottom of the cluster on first paint, which is the one
     moment this band exists for. min-height, so content always wins on short
     or zoomed viewports rather than being clipped. */
  min-height: calc(100svh - var(--header-h));
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* [MOBILE] Every one of these is restored at 60rem, so this is a phone-only
     change: the desktop hero keeps its own rhythm. The gap is the break between
     the jug cluster and the trust strip, so it is what makes the first screen
     end cleanly at the bottom of the cluster instead of clipping a card. */
  gap: var(--sp-5);
  padding-block: var(--sp-4);
  background: var(--color-surface-dark);
  color: var(--color-text-on-dark);
  overflow: hidden;
}

/* [MOBILE] Softer than --shadow-accent: half the blur and lower alpha, so the
   button reads as sitting on the dark ground rather than glowing off it. The
   HUE is untouched, this is the same --shadow-accent-rgb; only the spread and
   opacity move, and --color-action is not involved at all. Restored at 60rem. */
.hero .btn--accent {
  box-shadow:
    0 1px 2px rgb(var(--shadow-accent-rgb) / 0.14),
    0 4px 9px rgb(var(--shadow-accent-rgb) / 0.16);
}

/* The stage light. A ~1.25:1 tonal lift from --ink-raised, which is
   deliberately subliminal: it gives the cluster a lit space to stand in without
   becoming a visible graphic. A dark contact shadow is impossible on this
   ground (--shadow-jug measures 1.103:1 here, i.e. invisible), so this plus the
   shared baseline IS the grounding. */
.hero__stage {
  position: absolute;
  inset: 0;
  /* Positioned UNDER THE CLUSTER, not under the middle of the band. When the
     hero was a single column this sat at 50%, which was correct then. In the
     two-column layout the mass centres near 68% of the width, so a pool peaking
     at 50% was lighting the floor beneath the TEXT and leaving the jugs standing
     on unlit ground. Measured: no lift at all where the jugs actually are.
     68% puts the peak under the mass, 94% puts it at the contact line, and the
     falloff still reaches left behind the text so the two halves share one lit
     surface rather than each getting their own.
     The COLOUR is unchanged. This is a reposition, not a brightening: a visibly
     glowing floor on a near-black hero looks like a lighting effect rather than
     a room. --ink-raised is a 1.255:1 lift and that is all it should be. */
  background:
    /* 1. The floor light, under the mass.
       Deliberately narrow. At an 82% radius it still reached the left column and
       lifted the ground beneath the CTA, which dropped that button's boundary
       contrast against its surroundings from 3.12:1 to 2.73:1, under our 3:1
       floor for non-text. At 52% the pool goes fully transparent left of about
       x=463 at 1440, so it lights the cluster it is there for and leaves the
       button on clean --ink-deep. */
    radial-gradient(
      52% 50% at 70% 94%,
      var(--ink-raised) 0%,
      transparent 72%
    ),
    /* 2. Depth. The band falls away toward the top and the corners so it reads
       as a lit space rather than a flat rectangle. Deliberately a vignette
       rather than a top-to-bottom ramp: a linear ramp across a band this large
       is exactly what becomes a visible stripe, whereas a corner falloff has no
       edge to see.
       Black at 0.30, not a new colour: it darkens --ink-deep toward --ink-pure,
       which is already a token, and introduces no hue. Measured, the whole band
       spans about 14 luminance levels from its darkest corner to its lit floor,
       which is depth you feel rather than a transition you can point at.
       Contrast is unaffected in any way that matters: the headline measures
       17.0:1 at the darkest point and 12.8:1 at the lightest, and the lead 6.8:1
       at its worst. Both extremes are measured on the render, not predicted. */
    radial-gradient(
      125% 95% at 62% 105%,
      transparent 35%,
      rgba(0, 0, 0, 0.30) 100%
    );
  pointer-events: none;
}

/* Stacked on a phone, two columns of one row from 60rem. Not a .container,
   because the cluster needs viewport width to reach a useful size; the text
   column is instead aligned to the container's edge by the padding calc in the
   desktop block below, so the hero copy still lines up with every section under
   it. */
.hero__inner {
  position: relative;
  display: grid;
  /* [MOBILE] 28px. This is the only gap between the "Request a quote" button and
     the jug cluster, and at 16px the two were touching. Composed from tokens
     rather than a new step because the scale has nothing between 24 and 40.
     Desktop resets row-gap to --sp-5 in its own 60rem block. */
  gap: calc(var(--sp-4) + var(--sp-3));
  padding-inline: var(--gutter);
}

.hero__copy {
  position: relative;
}

.hero__title {
  font-size: var(--fs-900);
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-display);
}

/* --fs-500, not --fs-600. At --fs-600 this sets in more lines than the two-column
   text block can spare, and the lead is support copy under an 80px headline: a
   4.2x size ratio already gives it a clear subordinate role without extra size.
   One fixed value at every width, so this stays a single mobile-first size rather
   than a breakpoint swap. --col-primary matches the 30rem text column exactly, so
   the lead fills its column and needs no desktop override. */
.hero__lead {
  max-width: var(--col-primary);
  margin-block-start: var(--sp-2);
  font-size: var(--fs-500);
  line-height: 1.45;
  color: var(--color-text-muted-on-dark);
  text-wrap: pretty;
}

/* Column at base so a 320px phone never sets a button beside a link. */
.hero__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-4);
  margin-block-start: var(--sp-4);
}

/* The dark-ground link dialect, shared verbatim with .site-nav__link and
   .site-footer__link: starts dimmed, brightens to full, orange rule wipes in.
   NOT --color-accent-deep, which is 5.36:1 on the off-white body but only
   3.12:1 on this ground, a text-contrast failure. This pair is 8.59:1 rising
   to 16.03:1. --color-accent stays structural and never carries the text. */
/* [MOBILE] Hidden. On a phone the actions stack, so this sat alone between the
   button and the jug cluster, attached to neither. It is also redundant there:
   the cluster directly beneath it and the colour strip below both lead to the
   product line, and the header nav carries Products. Nothing is unreachable, and
   the hero loses a stranded element. It returns at 48rem, where the actions
   become a row and it reads as the button's secondary partner. */
.hero__link {
  display: none;
  position: relative;
  font-weight: var(--weight-medium);
  color: var(--color-text-muted-on-dark);
  text-decoration: none;
  padding-block-end: var(--sp-1);
  transition: color var(--dur-fast) var(--ease-standard);
}

.hero__link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: var(--border-thick);
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease-out);
}

.hero__link:hover { color: var(--color-text-on-dark); }
.hero__link:hover::after { transform: scaleX(1); }

/* --- The cluster.

   Everything is driven from ONE value, --cluster-h, the focal jug's height.
   Each jug then takes --s of that height, rises --r of it, sits at depth --z,
   and dims to --b. Widths follow from the image's own aspect ratio, so nothing
   here can distort a jug.

   MOBILE (base) is two overlapping rows of three, not the desktop fan: at 342px
   of usable width a six-wide fan gives each jug about 75px, too narrow to read
   as mass. Two rows buy roughly 40% more jug height for the same footprint. */
.cluster {
  position: relative;
  margin: 0;
}

/* auto columns plus justify-content, not 1fr: the columns size to the jugs so a
   negative inline margin produces real overlap. With 1fr tracks the jugs would
   sit centred in fixed cells with dead air between them, which is the catalogue
   look this rebuild exists to escape. */
.cluster__list {
  display: grid;
  grid-template-columns: repeat(3, auto);
  justify-content: center;
  align-items: end;
  justify-items: center;
  margin: 0;
  padding: 0;
  list-style: none;
}

.cluster__item {
  position: relative;
  z-index: var(--z);
  /* The rise. Bottom margin lifts the jug off the shared baseline, which is what
     lets a jug behind another still show its name band above the shoulder line
     in front of it. */
  margin-block-end: calc(var(--cluster-h) * var(--r));
}

/* height-driven, width:auto. Sizing by height keeps the whole cluster in one
   proportional system and keeps the aspect ratio untouched. */
.cluster__jug {
  display: block;
  position: relative;
  z-index: 1;
  width: auto;
  height: calc(var(--cluster-h) * var(--s));
  max-width: none;
  /* Atmospheric recession. Distance desaturates and dims in the real world, so
     the jugs further back dim slightly. Measured: at brightness 0.84 the bottle
     body still reads 11.68:1 against --ink-deep, so there is large headroom
     under the 4.5:1 floor. */
  filter: brightness(var(--b));
}

/* The contact shadow, and the reason it is visible at all.
   A dark shadow on a near-black ground is nothing: --shadow-jug measures
   1.103:1 here. So .hero__stage lifts the floor FIRST, to rgb(26,39,35), and the
   shadow is then cast onto that lifted floor, where near-black measures
   1.358:1. Real contact shadow, no invented light source.
   Wider than the jug so it spreads past the opaque base instead of hiding
   behind it, and pushed down so it reads as contact rather than as a halo. */
.cluster__shadow {
  position: absolute;
  z-index: 0;
  left: 50%;
  bottom: 0;
  width: calc(var(--cluster-h) * var(--s) * 0.86);
  height: calc(var(--cluster-h) * var(--s) * 0.11);
  transform: translate(-50%, 42%);
  background: radial-gradient(
    closest-side ellipse,
    rgba(0, 0, 0, var(--sh)) 0%,
    rgba(0, 0, 0, 0) 70%
  );
  pointer-events: none;
}

/* --- The per-jug ladder. Scale, rise, depth, dim, shadow weight, entrance lift.
   The focal jug is the anchor: largest, lowest, frontmost, undimmed, with the
   tightest darkest shadow. Everything else steps away from it. --- */
.cluster__item--blue   { --s: 1.00; --r: 0.00; --z: 5; --b: 1.00; --sh: 0.55; --lift: 3rem;   }
.cluster__item--red    { --s: 0.90; --r: 0.07; --z: 3; --b: 0.94; --sh: 0.46; --lift: 2.4rem; }
.cluster__item--green  { --s: 0.90; --r: 0.07; --z: 4; --b: 0.94; --sh: 0.46; --lift: 2.4rem; }
.cluster__item--purple { --s: 0.80; --r: 0.16; --z: 2; --b: 0.88; --sh: 0.36; --lift: 1.9rem; }
.cluster__item--yellow { --s: 0.80; --r: 0.16; --z: 2; --b: 0.88; --sh: 0.36; --lift: 1.9rem; }
/* [MOBILE] OrangeZone matches the rest of the back row: same scale, same rise, so
   the three read as one level rank behind the front three. The fan's values,
   which make it the smallest and furthest back, are restored at 60rem where it is
   the tail of a fan rather than a member of a row. */
.cluster__item--orange { --s: 0.80; --r: 0.16; --z: 1; --b: 0.84; --sh: 0.28; --lift: 1.5rem; }

/* [MOBILE] Explicit placement, because the fan's DOM order is a left-to-right
   composition order and the mobile grid needs the three largest in front.
   Back row: orange, purple, yellow. Front row: red, blue, green. */
.cluster__item--orange { grid-area: 1 / 1; }
.cluster__item--purple { grid-area: 1 / 2; }
.cluster__item--yellow { grid-area: 1 / 3; }
.cluster__item--red    { grid-area: 2 / 1; }
.cluster__item--blue   { grid-area: 2 / 2; }
.cluster__item--green  { grid-area: 2 / 3; }

/* [MOBILE] The front row climbs into the back row so the two read as one mass
   rather than two rows. The back row keeps its own rise on top of this.

   0.28, not 0.48. At 0.48 the front row swallowed the bottom half of the back
   jugs, and since the printed name sits at 35 to 53% of a jug's height, the back
   three labels were right on the edge of being covered: the group read as a pile
   rather than a lineup with depth. At 0.28 the back row's whole label band clears
   the front row while the two still overlap enough to read as one mass. */
.cluster__item--red,
.cluster__item--blue,
.cluster__item--green {
  margin-block-start: calc(var(--cluster-h) * -0.28);
}

/* [MOBILE] Horizontal overlap, scaled per jug so the amount stays a constant
   share of each jug's own width rather than a constant number of pixels. */
.cluster__item {
  margin-inline: calc(var(--cluster-h) * var(--s) * -0.035);
}

/* [MOBILE] The two outer back jugs pull inward so the back row gathers behind the
   front row instead of splaying to the edges. PurpleZone is the axis and does not
   move.
   Relative offset rather than margin, deliberately: a margin would widen the auto
   grid columns and push the cluster past the 342px of usable width it currently
   fills exactly. `left` shifts the paint without touching layout. */
.cluster__item--orange { left: calc(var(--cluster-h) * 0.09); }
.cluster__item--yellow { left: calc(var(--cluster-h) * -0.09); }

/* --- Trust strip. Static at t=0, no .reveal. Four glass cards anchoring the
   base of the dark hero: 2x2 on a phone, one row of four from 40rem.

   The hairline rule that used to sit above the strip is gone. With each point in
   its own panel the cards themselves do the separating, and keeping both would
   have been two devices doing one job. ---------------------------------- */
/* Same content box as .hero__inner, not .container. As a container the strip
   stopped 112px short of the cluster's right edge, so the four cards sat inset
   from the composition above them and the group read as lopsided. Sharing the
   hero's box means the strip is exactly as wide as the thing it anchors. */
.trust {
  position: relative;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3);
  margin: 0;
  padding-inline: var(--gutter);
  list-style: none;
}

/* Glass, not buttons. The fill and border are the faintest values that still
   describe a panel; the radius is --radius-md rather than the pill or the large
   card radius so it reads as a surface rather than a control, and there is no
   shadow, no hover and no cursor change, because none of these is interactive.

   --fs-100 is the site's 14px floor and this sits on it, not below. Full
   --color-text-on-dark rather than the muted tone: these are assurances, and at
   14px the muted tone would read as fine print. */
.trust__item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  background: var(--glass-fill);
  border: var(--border-hairline) solid var(--glass-border);
  border-radius: var(--radius-md);
  font-size: var(--fs-100);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-on-dark);
  transition:
    background-color var(--dur-base) var(--ease-standard),
    border-color var(--dur-base) var(--ease-standard);
}

/* Light on glass. Nothing moves: no transform, no scale, no translate, so the
   card gains presence without the row twitching under the cursor.
   NOT paired with :focus-visible, because these are <li> elements carrying text.
   They are not links or buttons and cannot take focus, and giving them tabindex
   to enable a focus style would add four non-actionable tab stops, which is an
   accessibility regression rather than an affordance. */
.trust__item:hover {
  background-color: var(--glass-fill-hover);
  border-color: var(--glass-border-hover);
}

/* The spotlight edge: a 1px ring filled with a radial gradient, brightest at the
   top centre, as if a light above the band were catching the rim of the glass.
   Drawn by masking out the card's interior so only the ring paints.

   Gated behind @supports because without mask-composite this pseudo-element
   would render as a filled rectangle over the whole card, which is a far worse
   result than simply not having the ring. Where it is unsupported the fill and
   border still brighten, so the hover reads either way. */
@supports ((mask-composite: exclude) or (-webkit-mask-composite: xor)) {
  .trust__item::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    padding: var(--border-hairline);
    background:
      radial-gradient(
        75% 150% at 50% 0%,
        rgba(242, 240, 236, 0.40) 0%,
        rgba(242, 240, 236, 0.10) 45%,
        transparent 75%
      );
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    mask-composite: exclude;
    opacity: 0;
    transition: opacity var(--dur-base) var(--ease-standard);
    pointer-events: none;
  }

  .trust__item:hover::before {
    opacity: 1;
  }
}

/* Icon left, label right, both vertically centred, text left-aligned. The label
   is NOT centred in the card on purpose: a left rail inside each card lines the
   four icons up with each other and reads more considered than four centred
   pairs floating in four boxes. */
.trust__label {
  min-width: 0;
}

/* One box, one stroke weight, one colour for all three line marks. fill:none and
   stroke:currentColor are set here rather than on the SVGs so the icons can never
   drift apart, and so they always match the label they sit beside. */
.trust__icon {
  flex: 0 0 auto;
  width: 1.25rem;
  height: 1.25rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

@media (min-width: 40rem) {
  .trust {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

@media (min-width: 60rem) {
  /* Mirrors .hero__inner exactly, so the strip and the composition above it
     share one left rail and one right edge. */
  .trust {
    padding-inline:
      max(var(--gutter), calc(50% - var(--container) / 2 + var(--gutter)))
      var(--gutter);
  }
}

/* The caption carries the line for screen readers and for anyone who cannot
   load the images. The six img alts are empty on purpose so the cluster is
   announced once, not six times. */
.cluster__caption {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Without this the button and the link stay stacked at every width, which costs
   56px of vertical budget for no reason. */
@media (min-width: 48rem) {
  .hero__actions {
    flex-direction: row;
    align-items: center;
    gap: var(--sp-6);
  }
  /* Back, now that it has a row to sit in beside the button. */
  .hero__link { display: inline-block; }
}

@media (min-width: 60rem) {
  .hero {
    /* The desktop focal-jug height. Set here rather than in the token layer
       because the arrangement changes at this breakpoint: two rows of three
       below, a six-wide fan above.
       Width term: the six jug widths sum to 3.580 focal-heights (scales summing
       to 5.10, each 0.702 as wide as it is tall) and the five overlaps subtract
       0.308, so the fan spans 3.272 focal-heights. The cluster column measures a
       steady ~52vw from 1200px up, because the text column is a fixed 30rem while
       everything else scales. Divided by 3.3 rather than 3.272 to leave the fan a
       little room inside its column instead of exactly filling it.
       The svh term caps it on short windows. Smaller wins. */
    --cluster-h: min(calc(52vw / 3.3), 46svh);
    gap: var(--sp-4);
    padding-block: var(--sp-5);

    /* Completes the phone-only reset the comment on the base rule promises. The
       base sets min-height: 100svh - header and justify-content: center for the
       phone hero; this block previously reset --cluster-h, gap and padding-block
       but NOT the height binding, so the full-viewport treatment leaked into every
       width from 60rem up.

       It is invisible on a normal desktop and severe on a tall one, because the
       desktop hero's content is only about 518px: the two columns sit side by side
       rather than stacked, so the taller the viewport the more surplus there is for
       justify-content: center to split.

         1440x900   hero  828px, 131px dead each side   the approved look
         1920x1080  hero 1008px, 221px dead each side
         960x1200   hero 1128px, 298px dead each side
         1024x1366  hero 1294px, 376px dead each side   iPad Pro 12.9 portrait

       Capped rather than removed. The full-screen hero is the point of this band
       on a normal desktop, and 1440x900 is the size it was composed against, so
       the cap is that height: 52rem is 832px against the 828px the reference
       viewport produces. Every desktop at or below about 900px tall is therefore
       bit-identical to before, and anything taller stops growing instead of
       stranding the composition in the middle of a void.

       justify-content stays center. With the height capped the surplus is bounded
       at roughly the same proportion the approved 1440x900 view already has, and
       centring is what produces that view. It was never the problem; the unbounded
       height was. */
    --hero-max-h: 52rem;
    min-height: min(calc(100svh - var(--header-h)), var(--hero-max-h));
  }

  /* Restore everything the mobile pass tightened, so those changes are phone-only:
     the inner row gap, the two copy margins, and the full-strength action shadow.
     Without these the desktop hero inherits the tighter mobile rhythm and the
     softened glow, which measured as a 6px shift and a visibly lighter button. */
  .hero__inner { row-gap: var(--sp-5); }
  .hero__lead { margin-block-start: var(--sp-3); }
  .hero__actions { margin-block-start: var(--sp-5); }
  .hero .btn--accent { box-shadow: var(--shadow-accent); }

  /* Two columns of ONE row. align-items:center is what interlocks the two
     masses: both occupy the same vertical band, so the cluster rises alongside
     the headline instead of sitting beneath it and there is no horizontal seam.
     The start padding aligns the text column with the container's content edge
     while the cluster still gets the full remaining viewport width. */
  .hero__inner {
    grid-template-columns: minmax(0, 30rem) minmax(0, 1fr);
    /* end, not center. Centring the cluster left it floating in the middle of its
       column with a dead void under it, and the raised outer jugs read as
       hovering because nothing agreed on where the floor was. Ending both columns
       on the same line gives the whole frame ONE ground plane: the jug bases, the
       CTA and the trust rule all resolve to it, which is what makes the two
       masses read as one composition rather than two adjacent objects. */
    align-items: end;
    column-gap: var(--sp-6);
    padding-inline:
      max(var(--gutter), calc(50% - var(--container) / 2 + var(--gutter)))
      var(--gutter);
  }

  /* Pull the mass back across the column boundary so the two halves read as one
     scene instead of two panels separated by a river. Measured at 1440: the copy
     column's box edge is x=632.5 and the cluster began at x=672.5, a visible 40px
     channel.

     The clearance that matters is not the column edge but the rightmost GLYPH
     inside the cluster's own vertical band. The longest headline line, "and
     coolant," at x=611, sits ABOVE the cluster entirely; within the band the
     rightmost text is the secondary link at about x=590. So there is far more
     room here than the column edge implies, and the jug's own transparent canvas
     padding adds about 5px more. Verified by measuring every text box in the band
     against the cluster's left edge on the render. */
  .cluster {
    margin-inline-start: -3.5rem;
  }

  /* The fan: one row, bottoms on a shared baseline with each rise measured up
     from it, mass centred in its column. */
  .cluster__list {
    display: flex;
    align-items: flex-end;
    justify-content: center;
  }

  /* Undo the mobile two-row placement, its inward nudges, and its levelled back
     row. The fan wants OrangeZone smallest and furthest back. */
  .cluster__item {
    grid-area: auto;
    margin-block-start: 0;
    margin-inline: 0;
    left: 0;
  }

  .cluster__item--orange { --s: 0.70; --r: 0.24; }

  /* The overlap, and the number the label-legibility argument rests on.

     Scaled by the jug's OWN --s, not a flat pixel amount. A flat amount is a
     larger share of a smaller jug: at a fixed -0.0772 the blue/purple pair
     measured 15.7% while red/orange measured 12.2%, and blue has only 16% of
     clear margin, so the smallest jug was the most overlapped. Scaling by --s
     holds every pair near 11% of the narrower jug's width.

     Measured clear margins beside the label field are 13-16% on the three jugs
     on the left flank and 13% on GreenZone, so 11% stays inside the margin on
     five of six. YellowZone's 7% is the tightest and is read back first. */
  .cluster__item + .cluster__item {
    margin-inline-start: calc(var(--cluster-h) * var(--s) * -0.070);
  }
}

/* --- 2. Company band. A half-step lift off the hero's off-white, which gives
   the page tonal structure without going dark this early. ---------------- */
.company {
  padding-block: var(--sp-7);
  background: var(--color-surface);
}

.company__inner {
  display: grid;
  gap: var(--sp-5);
}

.company__heading {
  font-size: var(--fs-700);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-display);
}

.company__body {
  display: grid;
  gap: var(--sp-5);
  max-width: var(--measure);
}

.company__text {
  font-size: var(--fs-400);
  line-height: var(--lh-body);
  color: var(--color-text-muted);
}

/* The independent-lab line is a credibility claim, not body copy, so it is
   given its own weight and an accent rule rather than being buried mid
   paragraph. Text stays --color-text (15.6:1), never the accent, which is a
   structural color only. */
.company__proof {
  padding-inline-start: var(--sp-4);
  border-inline-start: var(--border-thick) solid var(--color-accent);
  font-size: var(--fs-500);
  font-weight: var(--weight-medium);
  line-height: var(--lh-body);
  color: var(--color-text);
}

@media (min-width: 60rem) {
  .company { padding-block: var(--sp-8); }
  .company__inner {
    grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
    gap: var(--sp-8);
    align-items: start;
  }
}

/* --- 3. The color reveal. Six fields butted together with NO gap so the row
   reads as one continuous object rather than as six cards. That is the whole
   mechanic, so nothing in here may introduce a gap, a radius, or a transform
   that would open a seam between panels. ---------------------------------- */
/* One rhythm value for the whole section, so the two text bands are symmetric by
   construction rather than by four numbers that happen to agree. The band above
   the strip is (padding-top | heading | this), the band below is (this | link |
   padding-bottom), so using --spectrum-gap for all four makes each text group sit
   dead centre in its own band. Previously they were 96/40 and 40/64, both
   visibly lopsided. */
/* Two rhythms, not one. The band ABOVE the strip is a section intro and takes the
   full --spectrum-gap. The band BELOW is a footer to the strip, so it takes the
   smaller --spectrum-foot-gap: the link belongs to the colours above it, not to
   a section of its own. Each band is still symmetric within itself (the gap above
   the link equals the padding below it), so nothing floats off-centre.
   Earlier this over-corrected: fixing "crushed" by giving the foot the same 96px
   as the head left the link stranded in a void. --sp-6 is the tight one. */
.spectrum {
  /* [MOBILE] 0.8 of --sp-7, about 51px: roughly 20% off, still spacious for a
     two-line intro. Desktop sets its own --spectrum-gap at 60rem. */
  --spectrum-gap: calc(var(--sp-7) * 0.8);
  --spectrum-foot-gap: var(--sp-6);
  padding-block: var(--spectrum-gap) var(--spectrum-foot-gap);
}

/* Centred, and built as the deliberate sister of .close__inner: same grid, same
   justify-items, same --sp-5 gap, same type tokens, so the section that opens the
   colour system and the one that closes the page read as a matched pair. The type
   already matched (--fs-700 / --lh-heading / --tracking-display for the heading,
   --fs-400 / --lh-body / --color-text-muted for the sub); only the alignment and
   the measure differed.
   To go back to left-aligned: drop justify-items and text-align here. */
.spectrum__head {
  display: grid;
  justify-items: center;
  gap: var(--sp-5);
  margin-block-end: var(--spectrum-gap);
  text-align: center;
}

.spectrum__heading {
  max-width: var(--container-narrow);
  font-size: var(--fs-700);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
}

/* balance, not pretty. Both are values of the same property so this is a swap,
   not an addition. pretty only protects the LAST line from a stranded word,
   which is the right tool for a long left-aligned paragraph; here the copy sets
   in two lines and pretty left "of engines." dangling alone, which reads as an
   accident once the block is centred. balance evens the two lines instead, and
   it is copy-agnostic: reflowing it with a measured max-width would have to be
   re-tuned every time this sentence changes.
   The max-width stays because it is a token, not a hardcoded value, it is what
   makes this wrap to two lines at all, and it keeps the measure identical to
   .close__text, the element this is deliberately a sister to. */
.spectrum__lead {
  max-width: var(--col-primary);
  font-size: var(--fs-400);
  line-height: var(--lh-body);
  color: var(--color-text-muted);
  text-wrap: balance;
}

/* Full-bleed: the strip runs edge to edge, outside the container, because a
   gutter on either side would make it read as a wide card instead of as the
   line itself. */
.spectrum__list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.spectrum__cell {
  display: grid;
}

/* aspect-ratio, not a fixed height, so the panel stays a swatch at every
   width. Two-across on a phone is 4:3-ish; six-across on a desktop is tall
   and narrow, which is what makes the row read as a chart. */
.spectrum__panel {
  display: flex;
  align-items: flex-end;
  aspect-ratio: 4 / 3;
  padding: var(--sp-4);
  background: var(--fam-field);
  color: var(--fam-on);
  text-decoration: none;
  transition: box-shadow var(--dur-base) var(--ease-out);
}

.spectrum__name {
  font-family: var(--font-heading);
  font-size: var(--fs-500);
  font-weight: var(--weight-heading);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-tight);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: var(--border-thick);
  text-underline-offset: var(--underline-offset);
  transition: text-decoration-color var(--dur-base) var(--ease-out);
}

/* An inset ring drawn with inset box-shadow, NOT a transform and NOT an
   outline. A transform would lift the panel and open a gap on both sides of
   it, breaking the one continuous object the band exists to be. The ring uses
   the panel's own --fam-on, the single color already measured as legible on
   all six fields. Siblings are deliberately NOT dimmed: the palette is the
   payoff and it stays at full strength. */
.spectrum__panel:hover {
  box-shadow: inset 0 0 0 var(--border-thick) var(--fam-on);
}

.spectrum__panel:hover .spectrum__name {
  text-decoration-color: currentColor;
}

/* Same reasoning as .family:focus-visible: --color-focus is brand orange and
   is invisible on the orange field, so the panel's own ink is the only ring
   guaranteed to show on all six. Inset, because an outside ring on a
   full-bleed color field reads as a gap rather than as focus. */
.spectrum__panel:focus-visible {
  outline: var(--border-thick) solid var(--fam-on);
  outline-offset: calc(var(--focus-offset) * -1 - 1px);
  border-radius: 0;
}

/* Centred under the full-bleed strip, and given real space on BOTH sides so it
   belongs to this section instead of floating in the gap before the next one.
   Previously .spectrum had zero bottom padding, so the link sat 40px under the
   strip and then immediately met the following section: visually crushed, and
   ambiguous about which section it belonged to. */
.spectrum__foot {
  margin-block-start: var(--spectrum-foot-gap);
  text-align: center;
}

/* Weight bumped to bold, which is not a new style: --weight-bold is what
   .map__out uses, and that is the site's recipe for a STANDALONE call-out link,
   one with no surrounding sentence to mark it as a link. This is the same kind
   of link, so it takes the same weight.
   Everything else is untouched. The underline stays visible at rest and the
   colour stays --color-accent-deep (5.36:1 on the off-white ground), because
   that is the light-ground link dialect used across the site. No arrow: the only
   arrow mark in the system, .jump__arrow, belongs to the product-detail jump row,
   and reusing it here would either couple two unrelated components or duplicate
   it. Consistency beats the extra affordance. */
.spectrum__more {
  font-size: var(--fs-500);
  font-weight: var(--weight-bold);
  color: var(--color-accent-deep);
  text-decoration-thickness: var(--underline-thickness);
  text-underline-offset: var(--underline-offset);
}

/* inline-block on purpose. Text decoration is drawn by the ancestor and cannot
   be switched off by a descendant, but an inline-block establishes its own
   formatting context and drops out of the run, so the arrow sits beside the
   underline rather than on top of it.
   The nudge is the same device as .jump__arrow on the product pages, so the
   hover stays inside a pattern the site already has rather than becoming a
   one-off. Colour and underline are untouched. */
.spectrum__arrow {
  display: inline-block;
  margin-inline-start: 0.3em;
  transition: transform var(--dur-base) var(--ease-out);
}

.spectrum__more:hover .spectrum__arrow,
.caps__more:hover .caps__arrow {
  transform: translateX(0.2em);
}

@media (min-width: 40rem) {
  .spectrum__list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (min-width: 60rem) {
  /* One value again, so both bands stay symmetric at the larger desktop rhythm. */
  /* [DESKTOP] --sp-7, one step down from --sp-8. The value is used twice, as the
     section's padding-top and as the head's margin-bottom, so a single step takes
     64px out of the intro band: 317px down to 253px against a 273px colour strip,
     where it had been 192px of dead space around 125px of type. The two bands now
     read as siblings without their heights being tied to each other.
     Mobile keeps its own --spectrum-gap from the base rule and is untouched. */
  .spectrum { --spectrum-gap: var(--sp-7); }
  .spectrum__list { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  /* Was 1 / 2.4, a 570px showcase, when this band was the page's color reveal.
     Color now leads in the hero, so a second full-height color event would
     compete with it and read as repetition. At 1 / 1.15 the strip is still one
     continuous seam-free object with real presence as the second beat, but it
     clearly answers to the hero rather than rivalling it. */
  .spectrum__panel { aspect-ratio: 1 / 1.15; }
}

/* --- 4. The plant. Contained panel, not full bleed: the source is 1280px
   wide and 1.125, so a wide bleed would upscale it and crop it badly. ----- */
.reel {
  padding-block: var(--sp-7);
}

.reel__inner {
  display: grid;
  gap: var(--sp-6);
  align-items: center;
}

/* The panel carries the video's true ratio, so cover crops nothing from the
   video itself. It is there to reframe the poster, which is a 4:3 photo. */
.reel__media {
  aspect-ratio: 1.125;
  max-width: 40rem;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--color-surface-sunken);
  box-shadow: var(--shadow-lg);
}

.reel__video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.reel__body {
  display: grid;
  gap: var(--sp-4);
}

.reel__heading {
  font-size: var(--fs-700);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-display);
}

.reel__text {
  max-width: var(--measure);
  font-size: var(--fs-400);
  line-height: var(--lh-body);
  color: var(--color-text-muted);
}

@media (min-width: 60rem) {
  .reel { padding-block: var(--sp-8); }
  .reel__inner {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    gap: var(--sp-8);
  }
}

/* --- 5. Capability. Every figure in here is a locked fact, and the dark
   ground is where the one big number belongs. ---------------------------- */
.caps {
  padding-block: var(--sp-7);
  background: var(--color-surface-dark);
  color: var(--color-text-on-dark);
}

.caps__heading {
  font-size: var(--fs-700);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-display);
}

.caps__stat {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin-block: var(--sp-6);
  padding-block-end: var(--sp-6);
  border-block-end: var(--border-hairline) solid var(--color-border-on-dark);
}

/* min-width reserves the counter's widest state so the unit beside it is not
   shoved sideways on every tick as the number grows from one digit to six.

   3.1em is measured off the RENDER, not computed from the font tables. The
   advance sum says 3.0588em (every Open Sans 800 digit is exactly 0.5859em, so
   the face is already tabular and font-variant-numeric buys nothing here, plus a
   0.3091em comma, less --tracking-display). But Chrome actually lays "13,000"
   out at 3.0815em, about 2px wider at --fs-800's 88px cap, and reserving the
   theoretical figure left the label shifting by exactly that 1.9px. em, so it
   tracks --fs-800 at every viewport.

   inline-block is inert while this is a flex item, since flex blockifies it. It
   is kept so the reservation still holds if .caps__stat ever stops being flex. */
.caps__stat-value {
  display: inline-block;
  min-width: 3.1em;
  font-family: var(--font-heading);
  font-size: var(--fs-800);
  font-weight: var(--weight-heading);
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-display);
  color: var(--color-text-on-dark);
}

.caps__stat-unit {
  font-size: var(--fs-500);
  color: var(--color-text-muted-on-dark);
}

.caps__list {
  display: grid;
  gap: var(--sp-6);
  margin: 0;
  padding: 0;
  list-style: none;
}

.caps__item-title {
  font-size: var(--fs-500);
  font-weight: var(--weight-heading);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-tight);
}

.caps__item-text {
  margin-block-start: var(--sp-3);
  font-size: var(--fs-300);
  line-height: var(--lh-body);
  color: var(--color-text-muted-on-dark);
  text-wrap: pretty;
}

/* A real list rather than a sentence, used by the Capabilities columns. Home
   renders the same locked industries as one run-on line, which is fine for a
   teaser but buries "Government and municipal" mid-clause. A procurement reader
   scans for their own category before reading anything, so there each entry gets
   its own line. Same facts, same order, different typography.

   It lives here with the rest of .caps rather than in a page-local block, because
   the two pages now share one component and a second copy would be the thing that
   lets them drift apart. Colour matches .caps__item-text exactly. */
.caps__facts {
  margin-block-start: var(--sp-3);
  padding: 0;
  list-style: none;
  font-size: var(--fs-300);
  line-height: var(--lh-body);
  color: var(--color-text-muted-on-dark);
}

.caps__facts li {
  position: relative;
  padding-inline-start: var(--sp-4);
  padding-block: var(--sp-1);
}

/* Drawn from currentColor, so the marker tracks whatever the list text is and
   never needs a contrast budget of its own. */
.caps__facts li::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.72em;
  width: 0.35rem;
  height: 0.35rem;
  border-radius: 50%;
  background: color-mix(in srgb, currentColor 45%, transparent);
}

@media (min-width: 40rem) {
  .caps__list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 60rem) {
  .caps { padding-block: var(--sp-8); }
  .caps__stat {
    flex-direction: row;
    align-items: baseline;
    gap: var(--sp-5);
  }
  .caps__list { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* --- 6. Brand reprise. The wordmark on the sign carries the same six colors
   as the spectrum band, which is why it belongs below it. ----------------- */
.sign__img {
  display: block;
  width: 100%;
  height: auto;
}

/* --- 7. Close. Off-white, so the ink-deep footer below it still reads as a
   separate thing and the page keeps its last edge. ----------------------- */
.close {
  padding-block: var(--sp-8);
}

/* Centred, and that IS the fix for the lopsided right side. Left-aligning a
   short heading, one line of copy and a button inside a 75rem container leaves
   two thirds of the band empty on the right, which is what read as unbalanced.
   Centring removes the empty side entirely instead of filling it.
   Deliberately NOT a background graphic, watermark or tone block: this would be
   the site's first background image, and a decorated close undercuts the
   restraint the whole page is built on. The page is left-aligned throughout, so
   a centred final beat also reads as a deliberate change of register rather than
   an inconsistency, and it matches the centred link that closes the colour
   strip. */
.close__inner {
  display: grid;
  justify-items: center;
  gap: var(--sp-5);
  text-align: center;
}

.close__heading {
  max-width: var(--container-narrow);
  font-size: var(--fs-700);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
}

.close__text {
  max-width: var(--col-primary);
  font-size: var(--fs-400);
  line-height: var(--lh-body);
  color: var(--color-text-muted);
  text-wrap: pretty;
}

/* The spectrum band's arrow link, restated for a dark ground. It cannot simply
   reuse .spectrum__more: that rule is --color-accent-deep, which measures 3.12:1
   on ink and fails 4.5:1 as text. Same relationship as the secondary button, and
   the same resolution, --color-action-on-dark at 4.70:1. Structure, weight,
   underline and arrow behaviour are otherwise identical, so the two read as one
   device. */
.caps__more {
  display: inline-block;
  margin-block-start: var(--sp-6);
  font-size: var(--fs-500);
  font-weight: var(--weight-bold);
  color: var(--color-action-on-dark);
  text-decoration-thickness: var(--underline-thickness);
  text-underline-offset: var(--underline-offset);
}

.caps__arrow {
  display: inline-block;
  margin-inline-start: 0.3em;
  transition: transform var(--dur-base) var(--ease-out);
}

/* --------------------------------------------------------------------------
   10b. CAPABILITIES  [BOTH]
   Off-white content page, like Contact, with one ink band. Almost everything
   here is borrowed: .page-head, .caps, .close and the button tiers all arrive
   from sections above. What is new is the capability sheet and the photo grid.
   -------------------------------------------------------------------------- */

/* --- Federal contracting. A reference block, not a sales block: a procurement
   reader is looking up four values, so it is a definition list and the values
   are the loud part. Off-white like the rest of the page.

   The identifiers keep Open Sans rather than gaining a monospace family. The
   site ships one self-hosted typeface and no third-party font request, and a
   system mono stack would render differently on every machine. A UEI is still a
   string somebody transcribes into a form, so the legibility comes from
   --tracking-label instead: opening the letter-spacing separates the glyphs and
   makes the run read character by character, which is what transcription needs.
   ------------------------------------------------------------------------- */
/* Deliberately no background. .floor below is --color-surface-sunken, and when
   this section carried the same value the two stacked into one 1893px slab of
   identical grey with no boundary between "Federal contracting." and "On the
   floor." Sitting on the page ground instead gives the page a real alternation:
   dark stat block, page ground, sunken photo band, page ground for the close. */
.gov {
  padding-block: var(--sp-7);
}

.gov__heading {
  font-size: var(--fs-700);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-display);
}

.gov__lead {
  max-width: var(--col-primary);
  margin-block-start: var(--sp-3);
  font-size: var(--fs-400);
  line-height: var(--lh-body);
  color: var(--color-text-muted);
  text-wrap: pretty;
}

.gov__list {
  display: grid;
  gap: var(--sp-4);
  margin-block-start: var(--sp-6);
}

/* Hairline between rows rather than a boxed table: the page has no other table
   and a bordered one would read as a different system. */
.gov__row {
  display: grid;
  gap: var(--sp-1);
  padding-block-end: var(--sp-4);
  border-block-end: var(--border-hairline) solid var(--color-border);
}

.gov__row:last-child {
  padding-block-end: 0;
  border-block-end: 0;
}

.gov__term {
  font-size: var(--fs-200);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.gov__value {
  margin: 0;
  font-size: var(--fs-500);
  font-weight: var(--weight-bold);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-label);
  font-variant-numeric: tabular-nums;
}

/* The plain-language expansion of a code. Set back down to body voice so it
   reads as a gloss rather than a second value. */
.gov__note {
  display: block;
  margin-block-start: var(--sp-1);
  font-size: var(--fs-200);
  font-weight: var(--weight-body);
  /* Reset the tracking the value opened up. The gloss is prose, not a code. */
  letter-spacing: normal;
  line-height: var(--lh-body);
  color: var(--color-text-muted);
}

@media (min-width: 40rem) {
  /* Term and value side by side once there is room, which is how a reader scans
     a reference block. The term column is fixed so all four values align. */
  .gov__row {
    grid-template-columns: 9rem minmax(0, 1fr);
    gap: var(--sp-5);
    align-items: baseline;
  }
}

@media (min-width: 60rem) {
  .gov { padding-block: var(--sp-8); }
}

/* --- Facility photos. ------------------------------------------------------ */
/* Sunken rather than plain surface, so the photo band separates from the
   capability sheet above it without introducing a second light value of its
   own. Same token the map panel uses. */
.floor {
  padding-block: var(--sp-7);
  background: var(--color-surface-sunken);
}

.floor__heading {
  font-size: var(--fs-700);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-display);
}

.floor__lead {
  max-width: var(--col-primary);
  margin-block-start: var(--sp-3);
  font-size: var(--fs-400);
  line-height: var(--lh-body);
  color: var(--color-text-muted);
}

.floor__grid {
  display: grid;
  gap: var(--sp-5);
  margin-block-start: var(--sp-6);
  padding: 0;
  list-style: none;
}

.floor__fig {
  margin: 0;
}

/* Fixed ratio plus object-fit, so a photo that is not exactly 4:3 still fills
   its cell and the four cells stay on one baseline. The intrinsic width and
   height on the img mean the box is reserved before the file arrives, so a
   lazy-loaded photo cannot shift the caption under it. */
.floor__img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-md);
  background: var(--color-surface);
}

.floor__cap {
  margin-block-start: var(--sp-3);
  font-size: var(--fs-200);
  line-height: var(--lh-body);
  color: var(--color-text-muted);
}

.floor__made {
  margin-block-start: var(--sp-6);
  font-size: var(--fs-300);
  font-weight: var(--weight-bold);
  line-height: var(--lh-body);
}

@media (min-width: 40rem) {
  .floor__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 60rem) {
  .floor { padding-block: var(--sp-8); }
}

/* --------------------------------------------------------------------------
   10c. GALLERY  [BOTH]
   Off-white like Contact and Capabilities. Twelve uniform frames and one
   overlay. Every source is 1920x1440, so a fixed 4/3 ratio keeps every row on
   one baseline without masonry and without cropping surprises.
   -------------------------------------------------------------------------- */
/* Bottom padding is deliberately 0, not a smaller step.

   The closing band that follows sits on the SAME off-white ground as this one,
   so any bottom padding here stacks with .close's own 96px top padding. Measured,
   that was 192px of undifferentiated void between the last caption and the closing
   headline, with nothing to explain the distance.

   Home and Capabilities stack the same 192px and read fine, because the section
   above .close carries its own background there (ink, and sunken) so the boundary
   is drawn in colour and the space belongs to two visibly separate bands. Gallery
   has no such boundary, so the gap has to be drawn in space alone.

   Fixed at the source rather than halved on both sides: .close owns the whole
   transition, this side contributes nothing, and the two can never double again. */
.gallery {
  padding-block: var(--sp-7) 0;
}

.gallery__grid {
  display: grid;
  gap: var(--sp-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.gallery__fig {
  margin: 0;
}

/* A button, not a link: it opens an overlay rather than navigating. Stripped to
   the image, but it keeps a real focus ring from the base :focus-visible rule,
   which is the whole reason not to hang the handler on the img itself. */
.gallery__btn {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  border-radius: var(--radius-md);
  /* The image is the button's whole surface, so the corners have to clip or the
     focus ring and the photo disagree about the shape. */
  overflow: hidden;
}

.gallery__img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--color-surface);
  transition: transform var(--dur-base) var(--ease-out);
}

.gallery__cap {
  margin-block-start: var(--sp-3);
  font-size: var(--fs-200);
  line-height: var(--lh-body);
  color: var(--color-text-muted);
}

/* --- Lightbox. -------------------------------------------------------------
   Fixed, full viewport, above the sticky header. The scrim is a separate
   element rather than a background on the panel, so a click on the backdrop is
   an unambiguous hit test instead of an event-target comparison. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: var(--sp-5);
}

.lightbox[hidden] {
  display: none;
}

/* fixed, not absolute. The parent is a grid with place-items: center, and that
   sets align-self and justify-self on EVERY child including an absolutely
   positioned one. For an abspos grid item, a centred self-alignment sizes the
   box to its content instead of letting inset: 0 stretch it, so the scrim
   collapsed to 0x0 and the page showed straight through the overlay. Taking it
   out of the grid's alignment entirely is the fix; position: fixed resolves
   against the viewport, which is the box we actually want.
   The colour is the same ink the header and hero already use. No new colour. */
.lightbox__scrim {
  position: fixed;
  inset: 0;
  background: rgba(14, 23, 20, 0.92);
}

.lightbox__panel {
  position: relative;
  display: grid;
  justify-items: center;
  gap: var(--sp-3);
  width: 100%;
  max-width: 72rem;
}

.lightbox__fig {
  margin: 0;
  display: grid;
  justify-items: center;
  gap: var(--sp-3);
}

/* Bounded by BOTH axes. Width alone would let a landscape frame run past the
   bottom of a short window, and height alone would overflow a narrow one. */
.lightbox__img {
  display: block;
  max-width: 100%;
  max-height: 70svh;
  width: auto;
  height: auto;
  border-radius: var(--radius-md);
  transition:
    transform var(--dur-base) var(--ease-out),
    opacity var(--dur-base) var(--ease-out);
}

/* The sweep, and the only motion on this page. The class is added for one frame
   by script.js before the source swaps, so the outgoing frame steps aside in the
   direction of travel and the incoming one settles from the same side. */
.lightbox__img.is-sweep-next {
  transform: translateX(-2%);
  opacity: 0;
}

.lightbox__img.is-sweep-prev {
  transform: translateX(2%);
  opacity: 0;
}

.lightbox__caption {
  max-width: var(--measure);
  font-size: var(--fs-300);
  line-height: var(--lh-body);
  text-align: center;
  color: var(--color-text-on-dark);
  text-wrap: pretty;
}

.lightbox__counter {
  font-size: var(--fs-200);
  color: var(--color-text-muted-on-dark);
}

/* Controls read as controls on the scrim: a hairline ring and a fill, both
   derived from the on-dark pair already in the palette. */
.lightbox__btn {
  position: absolute;
  display: grid;
  place-items: center;
  width: var(--tap-min);
  height: var(--tap-min);
  padding: 0;
  cursor: pointer;
  color: var(--color-text-on-dark);
  background: rgba(242, 240, 236, 0.10);
  border: var(--border-hairline) solid var(--color-border-on-dark);
  border-radius: 50%;
  transition:
    background-color var(--dur-fast) var(--ease-standard),
    border-color var(--dur-fast) var(--ease-standard);
}

.lightbox__btn--prev { inset-inline-start: 0; inset-block-start: 50%; translate: 0 -50%; }
.lightbox__btn--next { inset-inline-end: 0; inset-block-start: 50%; translate: 0 -50%; }
.lightbox__btn--close { inset-inline-end: 0; inset-block-start: calc(var(--tap-min) * -1.2); }

/* Drawn from currentColor, so the glyphs inherit the button's contrast rather
   than carrying a second hardcoded value. */
.lightbox__chev,
.lightbox__x {
  display: block;
  width: 0.6rem;
  height: 0.6rem;
  border: 2px solid currentColor;
}

.lightbox__chev {
  border-block-end: 0;
  border-inline-end: 0;
}

.lightbox__btn--prev .lightbox__chev { rotate: -45deg; translate: 20% 0; }
.lightbox__btn--next .lightbox__chev { rotate: 135deg; translate: -20% 0; }

.lightbox__x {
  border: 0;
  position: relative;
  width: 0.85rem;
  height: 0.85rem;
}

.lightbox__x::before,
.lightbox__x::after {
  content: '';
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 0;
  width: 100%;
  height: 2px;
  background: currentColor;
}

.lightbox__x::before { rotate: 45deg; }
.lightbox__x::after { rotate: -45deg; }

/* Set by script.js while the overlay is open, so the page behind cannot be
   scrolled by wheel or touch. */
.has-lightbox-open {
  overflow: hidden;
}

@media (hover: hover) and (pointer: fine) {
  .gallery__btn:hover .gallery__img {
    transform: scale(1.03);
  }

  .lightbox__btn:hover {
    background: rgba(242, 240, 236, 0.18);
    border-color: var(--color-text-on-dark);
  }
}

@media (min-width: 40rem) {
  .gallery__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 60rem) {
  .gallery { padding-block: var(--sp-8) 0; }
  .gallery__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  /* Room for the side controls to sit outside the photo rather than over it. */
  .lightbox { padding-inline: var(--sp-8); }
  .lightbox__btn--prev { inset-inline-start: calc(var(--tap-min) * -1.4); }
  .lightbox__btn--next { inset-inline-end: calc(var(--tap-min) * -1.4); }
}

/* --------------------------------------------------------------------------
   10d. NOT FOUND  [BOTH]
   The way back, on the page-ground so it reads as a continuation of the page
   head above it rather than as a second band. Deliberately small: five routes
   and a line each. There is no search on this site, so a list of real
   destinations is the whole recovery path.
   -------------------------------------------------------------------------- */
.notfound {
  padding-block: 0 var(--sp-8);
}

.notfound__heading {
  font-size: var(--fs-500);
  font-weight: var(--weight-heading);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-tight);
}

.notfound__list {
  display: grid;
  gap: var(--sp-5);
  margin-block-start: var(--sp-5);
  padding: 0;
  list-style: none;
  max-width: var(--col-primary);
}

/* Hairline between rows, matching .gov__row. Not a boxed table, because the page
   has no other table and one here would read as a different system. */
.notfound__item {
  display: grid;
  gap: var(--sp-1);
  padding-block-end: var(--sp-5);
  border-block-end: var(--border-hairline) solid var(--color-border);
}

.notfound__item:last-child {
  padding-block-end: 0;
  border-block-end: 0;
}

/* The destination carries the weight; the note underneath is the explanation.
   Link colour is --color-action, the one link dialect the site already uses for
   an inline text link, at 5.84:1 on this ground. */
.notfound__link {
  font-size: var(--fs-500);
  font-weight: var(--weight-bold);
  color: var(--color-action);
  text-decoration-thickness: var(--underline-thickness);
  text-underline-offset: var(--underline-offset);
}

.notfound__note {
  font-size: var(--fs-300);
  line-height: var(--lh-body);
  color: var(--color-text-muted);
}

/* --------------------------------------------------------------------------
   11. SITE FOOTER  [BOTH]
   -------------------------------------------------------------------------- */
.site-footer {
  flex: 0 0 auto;
  margin-block-start: auto;
  background: var(--color-surface-dark);
  color: var(--color-text-on-dark);
  border-top: var(--border-thick) solid var(--color-accent);
}

.site-footer__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-6);
  align-items: start;
  padding-block: var(--sp-7);
}

@media (min-width: 52rem) {
  .site-footer__inner {
    grid-template-columns: 1.3fr 1fr 1.2fr;
    gap: var(--sp-6);
  }
}

/* The mark is a link home, matching the header brand. The margin belongs to the
   anchor, not the image, so the focus ring wraps the logo tightly instead of
   enclosing the gap below it. */
.site-footer__brand {
  display: inline-block;
  margin-block-end: var(--sp-4);
  border-radius: var(--radius-sm);
  transition: opacity var(--dur-fast) var(--ease-standard);
}

@media (hover: hover) and (pointer: fine) {
  .site-footer__brand:hover { opacity: 0.78; }
}

.site-footer__logo {
  height: var(--logo-h-footer);
  width: auto;
}

.site-footer__made {
  font-size: var(--fs-300);
  color: var(--color-text-muted-on-dark);
  max-width: 26ch;
}

.site-footer__heading {
  font-size: var(--fs-100);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-text-muted-on-dark);
  margin-block-end: var(--sp-4);
}

.site-footer__links {
  display: grid;
  gap: var(--sp-3);
}

/* --- Footer link hover. The header nav's mechanic, ported to the dark bar. ---
   The header does two things at once: the label brightens from
   --color-text-muted-on-dark to --color-text-on-dark over --dur-fast, and a
   2px --color-accent rule, absolutely positioned across the full width of the
   link box, wipes in from the left via scaleX(0) to scaleX(1) over --dur-base
   on --ease-out. The wipe is the part that makes it feel alive.
   All three of the header's moves are here, on the header's own tokens and
   timings so the two bars feel like one system:
     1. dim to bright, --color-text-muted-on-dark to --color-text-on-dark,
        --dur-fast on --ease-standard. 8.59:1 at rest, 16.03:1 hovered, so the
        dim end is emphasis and not a contrast concession.
     2. a --lift-sm rise, the primary button's token, over --dur-base.
     3. the 2px accent rule wiping in from the left, --dur-base on --ease-out.
   This is a positioned rule rather than a text-decoration, so it uses the
   header's --border-thick, not --underline-thickness. The plate's inline links
   keep the quieter text-decoration recipe: they are large values on a light
   ground reading as content, not a row of navigation. */
.site-footer__link {
  position: relative;
  justify-self: start;
  padding-block-end: var(--sp-1);
  font-size: var(--fs-300);
  color: var(--color-text-muted-on-dark);
  text-decoration: none;
  transition:
    color var(--dur-fast) var(--ease-standard),
    transform var(--dur-base) var(--ease-out);
}

.site-footer__link:hover { color: var(--color-text-on-dark); }

/* The wipe itself, shared by the nav column and the two single-line contact
   links. Identical geometry to .site-nav__link::after. */
.site-footer__link::after,
.site-footer__contact-link::after,
.site-footer__credit-link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: var(--border-thick);
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease-out);
}

.site-footer__link:hover::after,
.site-footer__contact-link:hover::after,
.site-footer__credit-link:hover::after { transform: scaleX(1); }

/* The rise is gated the way every other lift on the site is: a touch tap fires
   hover and would otherwise strand the link in its raised state. The wipe is
   ungated, matching the header nav. The nav column is the only thing that
   lifts; the contact details deliberately stay put. */
@media (hover: hover) and (pointer: fine) {
  .site-footer__link:hover { transform: translateY(var(--lift-sm)); }
}

.site-footer__contact {
  display: grid;
  gap: var(--sp-3);
  font-size: var(--fs-300);
  color: var(--color-text-muted-on-dark);
}
/* --- Footer contact details. -------------------------------------------------
   These three are reference facts the visitor may want to act on, not a
   navigation column, so they diverge from the nav links above on purpose:
   full white at rest, because a phone number that has to be hovered before it
   looks clickable is a phone number the visitor may never try, and no lift,
   because a contact detail being read and copied should hold still.
   What they keep is the orange feedback on hover, so the footer still answers
   the pointer in one language. */
.site-footer__contact-link {
  position: relative;
  justify-self: start;
  padding-block-end: var(--sp-1);
  color: var(--color-text-on-dark);
  text-decoration: none;
}

/* The address is the exception inside the exception. It wraps across a <br>, and
   the ::after wipe is a single absolutely positioned bar: on a two-line link it
   would stretch under the whole box and underline empty space beside the
   shorter line. A native text-decoration underlines each line box separately,
   which is the only treatment that is correct here. Same accent, same reveal,
   different mechanism because the shape of the link demands it. */
.site-footer__map-link {
  color: var(--color-text-on-dark);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: var(--underline-thickness);
  text-underline-offset: var(--underline-offset);
  transition: text-decoration-color var(--dur-fast) var(--ease-standard);
}
.site-footer__map-link:hover { text-decoration-color: var(--color-accent); }

/* The legal line keeps the hairline and owns the top of this block. The credit
   below it takes the bottom padding instead, so the two read as one closing
   group rather than as two stacked rules. */
.site-footer__legal {
  padding-block: var(--sp-4) 0;
  border-top: var(--border-hairline) solid var(--color-border-on-dark);
  font-size: var(--fs-200);
  color: var(--color-text-muted-on-dark);
}

/* The studio signature. One step below the copyright at --fs-100 and on the same
   muted token, which measures 8.59:1 on this ground and so clears the 4.5:1 body
   floor at either size. No second border: a rule here would make the footer end
   on two horizontal lines and read as a banner rather than a signature.
   The link itself is not styled here at all. It joins .site-footer__link and
   .site-footer__contact-link in the existing wipe selector above, so it inherits
   that dialect, its hover colour and its reduced-motion handling by construction
   rather than by a second copy that could drift. */
.site-footer__credit {
  padding-block-end: var(--sp-5);
  margin-block-start: var(--sp-2);
  font-size: var(--fs-100);
  color: var(--color-text-muted-on-dark);
}

.site-footer__credit-link {
  position: relative;
  color: var(--color-text-muted-on-dark);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-standard);
}

.site-footer__credit-link:hover { color: var(--color-text-on-dark); }

/* --------------------------------------------------------------------------
   12. MOTION  [BOTH]
   One authored moment: a single orchestrated entrance, staggered in reading
   order. Not an identical entrance repeated per section.

   Content is visible by default. The hidden start state applies only when the
   inline head script has set .js on <html>, so a JS failure or a no-script
   client still sees everything.
   -------------------------------------------------------------------------- */
.js .reveal {
  opacity: 0;
  transform: translateY(var(--reveal-shift));
}

.js .reveal.is-in {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--dur-reveal) var(--ease-out),
    transform var(--dur-reveal) var(--ease-out);
}

.reveal-group > .reveal:nth-child(1).is-in { transition-delay: 0ms; }
.reveal-group > .reveal:nth-child(2).is-in { transition-delay: var(--reveal-stagger); }
.reveal-group > .reveal:nth-child(3).is-in { transition-delay: calc(var(--reveal-stagger) * 2); }
.reveal-group > .reveal:nth-child(4).is-in { transition-delay: calc(var(--reveal-stagger) * 3); }
.reveal-group > .reveal:nth-child(5).is-in { transition-delay: calc(var(--reveal-stagger) * 4); }
/* Six exists because the product grid has six tiles. The cap after it matters
   more than the sixth step: without it, a future group of a dozen items would
   have no delay at all past the fifth, and every later item would land at once
   while the first five staggered. Holding everything from the sixth onward at
   the same delay keeps the entrance finite and the tail coherent. */
.reveal-group > .reveal:nth-child(6).is-in { transition-delay: calc(var(--reveal-stagger) * 5); }
.reveal-group > .reveal:nth-child(n+7).is-in { transition-delay: calc(var(--reveal-stagger) * 6); }

/* [BOTH] The product grid staggers by COLUMN, not by absolute index.
   The generic rules above delay on DOM position, which is right for a vertical
   stack but wrong for a grid: the observer marks each tile in as it intersects,
   and in a 3-up grid tiles 4 to 6 are a second visual row that crosses the
   viewport well after the first. Under the generic rules the last tile would
   still be sitting out a 350ms delay after it was already on screen, which
   reads as lag rather than as choreography.
   Restarting the count each row means every row plays the same short 0, 1, 2
   cascade at the moment that row actually arrives.
   Equal specificity to the rules above, so this must stay below them.
   [MOBILE] 1-up gives every tile its own row, so the stagger flattens to zero
   and each tile simply reveals when it arrives. */
@media (max-width: 39.999rem) {
  .family-grid > .reveal.is-in { transition-delay: 0ms; }
}

@media (min-width: 40rem) and (max-width: 59.999rem) {
  .family-grid > .reveal:nth-child(2n+1).is-in { transition-delay: 0ms; }
  .family-grid > .reveal:nth-child(2n+2).is-in { transition-delay: var(--reveal-stagger); }
}

@media (min-width: 60rem) {
  .family-grid > .reveal:nth-child(3n+1).is-in { transition-delay: 0ms; }
  .family-grid > .reveal:nth-child(3n+2).is-in { transition-delay: var(--reveal-stagger); }
  .family-grid > .reveal:nth-child(3n+3).is-in { transition-delay: calc(var(--reveal-stagger) * 2); }
}

/* [BOTH] The Home hero cluster: the one authored "wow" on the page.

   This overrides the generic .reveal geometry above for the jugs only. It moves
   further (--lift, per jug), takes longer, and adds a scale, because it is the
   single first-paint moment rather than a section arriving mid-scroll. Same
   reasoning as the product grid override: equal specificity, so it must stay
   below the generic rules.

   scale starts at 0.96, NEVER 0. Nothing in the real world appears from nothing,
   and the difference between 0.96 and 0 is the difference between premium and
   cartoonish.

   The cluster assembles OUTWARD FROM ITS ANCHOR, which is the composition's own
   logic rather than a generic sweep: the focal jug settles first and the group
   builds around it. Travel distance decreases with depth (--lift on the family
   classes), which is how parallax behaves and reinforces the recession.
   Settles at 270ms + 700ms = 970ms, inside a second. */
.js .cluster__item.reveal {
  opacity: 0;
  transform: translateY(var(--lift)) scale(0.96);
}

.js .cluster__item.reveal.is-in {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--dur-cluster) var(--ease-out),
    transform var(--dur-cluster) var(--ease-out);
}

/* Keyed to the family, not to nth-child, because the delay belongs to a jug's
   place in the DEPTH ladder and that is the same at both breakpoints even though
   the spatial arrangement changes.

   The selectors carry .js and .reveal purely for specificity. The rule above sets
   the `transition` SHORTHAND, which resets transition-delay to 0, and it is
   (0,4,0); a plain `.cluster__item--blue.is-in` is only (0,2,0) and loses, which
   silently flattened the whole stagger to 0ms. These match its weight and come
   after it. */
.js .cluster__item--blue.reveal.is-in   { transition-delay: 0ms; }
.js .cluster__item--red.reveal.is-in,
.js .cluster__item--green.reveal.is-in  { transition-delay: var(--cluster-stagger); }
.js .cluster__item--purple.reveal.is-in,
.js .cluster__item--yellow.reveal.is-in { transition-delay: calc(var(--cluster-stagger) * 2); }
.js .cluster__item--orange.reveal.is-in { transition-delay: calc(var(--cluster-stagger) * 3); }

/* The stage light fades up first so the jugs land into lit space rather than
   arriving alongside their own lighting.

   A one-shot keyframe rather than a transition, deliberately: there is no state
   change to transition FROM on first paint, and no JS sets a class here. This
   is the one case where a keyframe beats a transition, because it is a load
   animation and nothing can interrupt it. `both` holds the end state.
   Scoped to .js only, so a no-script client simply gets the lit stage. */
@keyframes hero-stage-lit {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.js .hero__stage {
  animation: hero-stage-lit var(--dur-slow) var(--ease-out) both;
}

/* Reduced motion means less movement, not a dead interface. Position and scale
   changes go away entirely. Opacity and color transitions stay, because they
   carry meaning (hover feedback, focus, the drawer appearing) and cause none of
   the vestibular trouble that motion does.
   Defined at the token layer so every later page inherits it. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto !important; }

  /* Content is simply present. No entrance. */
  .js .reveal,
  .js .reveal.is-in {
    opacity: 1;
    transform: none;
    transition: none;
  }

  /* The hero cluster needs restating at its own specificity, NOT because it is a
     special case but because the rule that gives it its bigger entrance is
     (0,3,0) and the generic reset above is only (0,2,0). Without this the six
     jugs would keep their 700ms rise-and-scale for exactly the users who asked
     for no motion. Equal specificity and later in the file, so this wins.
     The per-family delay rules are (0,2,0), so the delay reset needs restating
     here too. */
  .js .cluster__item.reveal,
  .js .cluster__item.reveal.is-in,
  .cluster__item.is-in {
    opacity: 1;
    transform: none;
    transition: none;
    transition-delay: 0ms;
  }

  /* No looping or keyframed motion anywhere. */
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }

  /* Strip every movement, keep the fades. The footer nav links lose the rise but
     keep the brightness ramp, and their rule still arrives, just without the
     wipe, per the ::after transition reset below. Note these are the :hover
     elements only. Putting the ::after in here would kill its scaleX(0) resting
     state and leave every footer link permanently underlined.
     The contact links are absent because they never had a transform to strip.
     The product tile loses its rise and its jug loses the scale, but the tile
     keeps the elevation change and the descriptor still reveals its underline,
     so the hover is still legible as a hover. */
  .btn:active,
  .btn--accent:hover,
  .site-header__brand:hover,
  .site-footer__link:hover,
  .family:hover,
  .family:active,
  .family:hover .family__jug,
  .spectrum__more:hover .spectrum__arrow,
  .caps__more:hover .caps__arrow,
  .gallery__btn:hover .gallery__img,
  .line-card__row:hover,
  .doc:hover,
  .map__load:active .map__load-pill,
  .map__load:hover .map__load-pill {
    transform: none;
  }

  .site-nav,
  .site-nav[data-open='true'] {
    transform: none;
    transition:
      opacity var(--dur-fast) var(--ease-standard),
      visibility 0s;
  }

  /* The underline rule would still sweep, so it just appears. */
  /* The chevron is in the transition list, deliberately NOT in the transform
     list above. Its rotation is what reports open versus closed, so removing
     the transform would delete the state indicator rather than the motion.
     Killing only the transition keeps the meaning and drops the movement. */
  .site-nav__link::after,
  .site-subnav__link::after,
  .site-nav__chevron,
  .jump__link::after,
  .jump__arrow,
  .spectrum__arrow,
  .caps__arrow,
  .gallery__img,
  .lightbox__img,
  .lightbox__btn,
  .site-footer__link::after,
  .site-footer__contact-link::after,
  .site-footer__credit-link::after,
  .nav-toggle__bar,
  /* The trust cards KEEP their hover glow, they just arrive instantly. Removing
     the state would delete the affordance; removing only the fade leaves it. */
  .trust__item,
  .trust__item::before {
    transition: none;
  }

  /* script.js already declines to add these classes under reduced motion, but
     killing the transition alone would turn the offset into a hard flicker if
     one ever did land. Neutralising the offset itself makes the swap genuinely
     instant rather than instantly wrong. Belt and braces on purpose: this is the
     same failure the video band and the count-up are each guarded three ways
     against. */
  .lightbox__img.is-sweep-next,
  .lightbox__img.is-sweep-prev {
    transform: none;
    opacity: 1;
  }
}
