/* 01-foundation.css: self-hosted faces, the token vocabulary, reset,
   typography, layout primitives, buttons, material and motion.
   FILE ORDER IS CASCADE ORDER across the numbered stylesheets; append, never
   insert (org pattern).

   Every rule is styled off --sf-* tokens. The per-site token block emitted in
   <head> (theme document + validated tenant overrides) is the ONLY per-tenant
   style payload. It is emitted on `html:root` (0,1,1) rather than `:root`
   (0,1,0) because this file loads AFTER it and would otherwise win the
   cascade on equal specificity: the values below are gunmetal FALLBACKS, and
   before that fix they were silently the palette of every site on the
   platform. See ThemeCssBuilder for the full note.

   Two layers of token live here rather than in the theme document: the
   scales that are identical on every site (type steps, line heights,
   tracking, spacing, elevation formulas, motion) and the derivations (radius
   ramp, section rhythm). Emitting those per tenant would put ~2KB of
   identical bytes into every uncacheable head. The theme document carries
   only what actually differs between the four themes. */

/* ================================================================
   self-hosted faces (docs/DESIGN.md: no Google Fonts on tenant sites,
   CSP font-src 'self'). Provenance, licence and byte cost per file are
   recorded in wwwroot/fonts/README.md. Both families are SIL OFL 1.1.

   The display and text faces are the SAME FAMILY at two widths of one
   variable design (Archivo, wdth 75 and wdth 100). That is deliberate:
   an unrelated display face used at one size reads as a template with a
   font swap, where a real width cut of the text family reads as one
   typographic voice. It is also cheaper than two families, and it means
   tightening a headline is a width and tracking decision rather than
   compensation for not having a display cut at all.
   ================================================================ */

/* Display: Archivo, width 75, weight axis 100-900 live. 35,672 bytes. */
@font-face {
    font-family: 'Archivo Condensed';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/fonts/archivo-condensed-latin.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;
}

/* Text: Archivo, width 100, weight axis 100-900 live. 34,928 bytes. */
@font-face {
    font-family: 'Archivo';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/fonts/archivo-text-latin.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;
}

/* Display, editorial themes: Bitter 700. 18,572 bytes. The weight RANGE is
   declared wider than the single weight shipped on purpose: it stops the
   browser synthesising a faux bold when a theme asks for 800, which looks far
   worse than rendering the real 700 drawing. */
@font-face {
    font-family: 'Bitter';
    font-style: normal;
    font-weight: 500 800;
    font-display: swap;
    src: url('/fonts/bitter-display-latin.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;
}

/* Metric-matched fallbacks. font-display:swap means the first paint uses a
   local face and the web font replaces it, and a replacement whose glyphs are
   a different width RELAYS OUT THE TEXT, which is a layout shift on the
   largest element on the page. These faces are the local fallback rescaled
   (size-adjust from the frequency-weighted mean character advance measured on
   the shipped woff2, ascent and descent from the real hhea metrics) so the
   swap is close to invisible and CLS stays at 0. The arithmetic is written up
   in wwwroot/fonts/README.md. */
@font-face {
    font-family: 'Archivo Condensed Fallback';
    src: local('Arial Narrow'), local('Arial'), local('Helvetica'), local('Liberation Sans');
    size-adjust: 83.9%;
    ascent-override: 104.6%;
    descent-override: 25%;
    line-gap-override: 0%;
}

@font-face {
    font-family: 'Archivo Fallback';
    src: local('Arial'), local('Helvetica'), local('Liberation Sans');
    size-adjust: 92.8%;
    ascent-override: 94.6%;
    descent-override: 22.6%;
    line-gap-override: 0%;
}

@font-face {
    font-family: 'Bitter Fallback';
    src: local('Georgia'), local('Times New Roman'), local('Liberation Serif');
    size-adjust: 109.4%;
    ascent-override: 85.5%;
    descent-override: 24.2%;
    line-gap-override: 0%;
}

/* ================================================================
   round-5 faces (theme roster: docs/design/ultraplan/theme-roster.md).
   Every file below is self-subset with pyftsubset from the upstream
   google/fonts TTF, never taken from the Google/Fontsource pipelines,
   because those silently strip small caps (smcp/c2sc) and the small
   caps are exactly why Anton SC, Big Shoulders, Source Sans 3 and
   Literata were chosen. Licence verification (all SIL OFL 1.1), the
   Reserved-Font-Name handling for Source Sans 3 (renamed 'SS3 Web'
   per OFL), byte costs, per-theme budgets and the fallback arithmetic
   are recorded in wwwroot/fonts/README.md. Each face's OFL.txt sits
   alongside its woff2.
   ================================================================ */

/* Display, big-woods: National Park, wght axis 200-800 live. 39,608 bytes.
   The routed-sign face; ss01/tnum retained. */
@font-face {
    font-family: 'National Park';
    font-style: normal;
    font-weight: 200 800;
    font-display: swap;
    src: url('/fonts/national-park-latin.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;
}

/* Text, big-woods: Wittgenstein, wght axis 400-900 live. 29,588 bytes.
   Carries real oldstyle/lining/tabular figures (onum/lnum/tnum). */
@font-face {
    font-family: 'Wittgenstein';
    font-style: normal;
    font-weight: 400 900;
    font-display: swap;
    src: url('/fonts/wittgenstein-latin.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;
}

/* Display, iron-press: League Gothic, wdth axis 75-100 live (default 100).
   17,532 bytes. Single-weight 1903 ATF revival: the declared weight RANGE
   stops the browser synthesising a faux bold over the one real drawing,
   same trick as Bitter above. */
@font-face {
    font-family: 'League Gothic';
    font-style: normal;
    font-weight: 400 700;
    font-stretch: 75% 100%;
    font-display: swap;
    src: url('/fonts/league-gothic-latin.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;
}

/* Text, iron-press: Literata at its opsz 12 text drawing, wght axis 400-700
   live. 49,360 bytes. smcp and the full figure set (onum for the oldstyle
   numerals in prose that are the iron-press signature) retained. The opsz
   axis is pinned per role because the full two-axis variable font is 153KB;
   the display drawing is the separate file below. */
@font-face {
    font-family: 'Literata Text';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('/fonts/literata-text-latin.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;
}

/* Display, walnut-club: Literata at its opsz 72 display drawing, wght axis
   400-700 live. 48,172 bytes. Declared to 800 so a heavier heading request
   clamps to the real 700 instead of synthesising. */
@font-face {
    font-family: 'Literata Display';
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url('/fonts/literata-display-latin.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;
}

/* Display, blackout: Anton SC static. 12,720 bytes. The small caps ride in
   ccmp as well as smcp/c2sc, so they are always on with zero
   font-feature-settings. One ultra-bold drawing; the declared range stops
   any synthesis on top of it. */
@font-face {
    font-family: 'Anton SC';
    font-style: normal;
    font-weight: 400 900;
    font-display: swap;
    src: url('/fonts/anton-sc-latin.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;
}

/* Display, desert-mesa: Big Shoulders, wght axis 100-900 live, opsz pinned
   at the 72pt display drawing (display-only role; the live two-axis file is
   74KB). 44,216 bytes. TRUE small caps (smcp/c2sc) retained. */
@font-face {
    font-family: 'Big Shoulders';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/fonts/big-shoulders-latin.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;
}

/* Text, desert-mesa and walnut-club: Source Sans 3, wght axis 200-900 live.
   38,764 bytes. Subsetting makes a Modified Version under the OFL and
   'Source' is a Reserved Font Name, so the internal names are rewritten to
   'SS3 Web'; the CSS family matches. smcp/c2sc/onum retained. */
@font-face {
    font-family: 'SS3 Web';
    font-style: normal;
    font-weight: 200 900;
    font-display: swap;
    src: url('/fonts/ss3-latin.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;
}

/* Display AND text, arctic (weights 400/600 only there); text, blackout:
   Geist, wght axis 100-900 live. 28,292 bytes. tnum retained for the
   arctic mono-metadata registers. */
@font-face {
    font-family: 'Geist';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/fonts/geist-latin.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;
}

/* Round-5 metric-matched fallbacks, same arithmetic as the three above:
   size-adjust is the face's frequency-weighted mean character advance
   divided by the fallback's, measured with fontTools on the shipped woff2
   (display faces measured at the 650 heading weight they render at, text
   faces at 400); ascent/descent are the real hhea metrics divided by the
   size adjustment. Numbers and method in wwwroot/fonts/README.md. Safari
   ignores the ascent/descent overrides (unsupported descriptors are
   simply dropped); the width match, the dominant CLS term, still holds. */
@font-face {
    font-family: 'National Park Fallback';
    src: local('Arial'), local('Helvetica'), local('Liberation Sans');
    size-adjust: 106.8%;
    ascent-override: 96.5%;
    descent-override: 28.1%;
    line-gap-override: 0%;
}

@font-face {
    font-family: 'Wittgenstein Fallback';
    src: local('Georgia'), local('Times New Roman'), local('Liberation Serif');
    size-adjust: 101.8%;
    ascent-override: 104.6%;
    descent-override: 42.7%;
    line-gap-override: 0%;
}

@font-face {
    font-family: 'League Gothic Fallback';
    src: local('Arial Narrow'), local('Arial'), local('Helvetica'), local('Liberation Sans');
    size-adjust: 71.9%;
    ascent-override: 134.5%;
    descent-override: 32.3%;
    line-gap-override: 0%;
}

@font-face {
    font-family: 'Literata Text Fallback';
    src: local('Georgia'), local('Times New Roman'), local('Liberation Serif');
    size-adjust: 107.7%;
    ascent-override: 109.3%;
    descent-override: 28.6%;
    line-gap-override: 0%;
}

@font-face {
    font-family: 'Literata Display Fallback';
    src: local('Georgia'), local('Times New Roman'), local('Liberation Serif');
    size-adjust: 111.1%;
    ascent-override: 105.9%;
    descent-override: 27.7%;
    line-gap-override: 0%;
}

@font-face {
    font-family: 'Anton SC Fallback';
    src: local('Arial Narrow'), local('Arial'), local('Helvetica'), local('Liberation Sans');
    size-adjust: 110.9%;
    ascent-override: 106%;
    descent-override: 29.7%;
    line-gap-override: 0%;
}

@font-face {
    font-family: 'Big Shoulders Fallback';
    src: local('Arial Narrow'), local('Arial'), local('Helvetica'), local('Liberation Sans');
    size-adjust: 99.4%;
    ascent-override: 99.1%;
    descent-override: 21.6%;
    line-gap-override: 0%;
}

@font-face {
    font-family: 'SS3 Fallback';
    src: local('Arial'), local('Helvetica'), local('Liberation Sans');
    size-adjust: 93.4%;
    ascent-override: 109.6%;
    descent-override: 42.8%;
    line-gap-override: 0%;
}

@font-face {
    font-family: 'Geist Fallback';
    src: local('Arial'), local('Helvetica'), local('Liberation Sans');
    size-adjust: 104.4%;
    ascent-override: 96.3%;
    descent-override: 28.3%;
    line-gap-override: 0%;
}

/* ================================================================
   token defaults (gunmetal). The theme block overrides these.
   ================================================================ */

:root {
    /* ---- ground and surfaces: the elevation ramp is LIGHTNESS, not borders ---- */
    --sf-bg: #0C0B0A;
    --sf-surface-sunken: #070706;
    --sf-surface: #151310;
    --sf-surface-alt: #1C1916;
    --sf-surface-raised: #24201B;
    --sf-hairline: rgba(255, 240, 224, 0.09);
    --sf-border: #2E2822;
    --sf-control-border: #6B6156;

    /* ---- ink ramp ---- */
    --sf-ink: #F7F3EC;
    --sf-text: #CAC1B5;
    --sf-text-soft: #BAB0A4;
    --sf-text-faint: #9E9488;

    /* ---- brand and accent ---- */
    --sf-brand: #C98F42;
    --sf-brand-strong: #E0A85C;
    --sf-brand-dim: #6B4C22;
    --sf-on-brand: #17110A;
    --sf-accent: #9AA6AC;
    --sf-accent-strong: #B3BEC3;
    --sf-on-accent: #0E1113;
    --sf-link: #DFAB63;

    /* ---- shell ---- */
    --sf-header-bg: #0A0908;
    --sf-header-ink: #F7F3EC;
    --sf-header-border: rgba(255, 240, 224, 0.1);
    --sf-footer-bg: #080706;
    --sf-footer-ink: #F7F3EC;
    --sf-footer-text: #A79C8F;

    /* ---- inverted band ---- */
    --sf-dark-bg: #090807;
    --sf-dark-ink: #F7F3EC;
    --sf-dark-text: #B5AB9E;

    /* ---- hero ---- */
    --sf-hero-grad-a: #141210;
    --sf-hero-grad-b: #2E2A22;
    --sf-hero-ink: #FFFFFF;
    --sf-hero-overlay: rgba(8, 7, 6, 0.62);

    /* ---- badges ---- */
    --sf-badge-bg: #262119;
    --sf-badge-ink: #DBD2C4;

    /* ---- status. Previously hardcoded #4CAF50 / #E6A23C / #B42318 in three
       stylesheets, which is why "in stock" was the same green on a near-black
       theme and a cream one and failed AA on the light ones. ---- */
    --sf-positive: #6FBF73;
    --sf-caution: #E0A85C;
    --sf-critical: #E2705F;

    /* Focus ring gets its own token pair, NOT --sf-brand: the brand colour is
       exactly what primary buttons are painted with, so a brand-coloured ring
       on a brand-coloured button is invisible. Two tones so one of them always
       contrasts, whatever surface the ring lands on. Themes may override both;
       overrides are AA-validated at save like every other token. */
    --sf-focus: #FFFFFF;
    --sf-focus-contrast: #0A0908;

    /* ---- typography ---- */
    --sf-font-body: 'Archivo', 'Archivo Fallback', system-ui, -apple-system, 'Segoe UI', sans-serif;
    --sf-font-heading: 'Archivo Condensed', 'Archivo Condensed Fallback', 'Arial Narrow', system-ui, sans-serif;
    --sf-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    /* Two weights of hierarchy, not five. A weight-driven hierarchy (400 body,
       800 headings, no size or tracking contrast) is the default of every CMS
       theme and the clearest tell there is. Hierarchy here comes from SIZE and
       TRACKING; weight barely moves. Display sits BELOW heading weight on
       purpose: apparent weight rises with size, which is what an optical-size
       axis would correct automatically if either of our faces had one. */
    --sf-body-weight: 400;
    --sf-heading-weight: 650;
    --sf-display-weight: 600;
    --sf-color-scheme: dark;
    --sf-heading-transform: none;
    --sf-display-transform: none;

    /* Per-theme tracking OFFSET applied on top of the size-derived tracking
       below. Zero here because the size-derived values are already drawn for
       this face; a serif theme dials them back, an uppercase theme dials them
       positive. */
    --sf-track-offset: 0em;
    --sf-track-offset-display: 0em;
    --sf-track-caps: 0.1em;
    --sf-eyebrow-transform: uppercase;

    /* ---- structure: the tokens that let a theme change page SHAPE ---- */
    --sf-display-scale: 1.06;
    --sf-rhythm: 1.12;
    --sf-measure-token: 51ch;
    --sf-density: 1;
    --sf-rule-weight: 1px;
    --sf-radius: 12px;
    --sf-radius-sm: 6px;
    --sf-btn-radius: 8px;
    --sf-container: 1320px;

    /* ---- material ---- */
    --sf-shadow-rgb: 32 22 12;
    --sf-shadow-strength: 1;
    --sf-edge-highlight: rgba(255, 244, 230, 0.055);
    --sf-edge-shade: rgba(0, 0, 0, 0.5);
    --sf-grain-opacity: 0.05;
    --sf-grain-blend: overlay;
    --sf-tint-strength: 0;

    /* ---- the signature ----

       One token the default kits do not ship, named and applied consistently.
       Every generated design system is drawing on the same default aesthetic,
       so the median output looks the same everywhere; a fingerprint is what
       stops that, and here it is part of the THEME DOCUMENT so all four
       themes get a different one and every dealer site inherits it. This is
       the product, not a flourish: "70% of gun stores have the same layout"
       is the complaint the platform exists to answer.

       signature-edge is the treatment on every raised surface and section
       seam. ease-signature is the curve every transition on the site runs. */
    --sf-signature-edge: inset 0 1px 0 rgba(255, 236, 210, 0.14), inset 0 0 0 1px rgba(255, 240, 224, 0.05);
    --sf-ease-signature: cubic-bezier(0.23, 1, 0.32, 1);

    /* ================================================================
       Below here: scales shared by every site, so they are NOT in the
       theme document and never cost a per-tenant byte.
       ================================================================ */

    /* ---- fluid type scale, two ratios (Utopia method) ----

       1.2 at a 360px viewport, 1.25 at 1240px, base 16px to 17.5px. Two
       ratios rather than one is the whole point: hierarchy EXPANDS with the
       canvas instead of everything scaling together. Measured on this scale,
       body grows 9.4% between the two ends while the h1 step grows 34.1% and
       the display step grows 129%. That widening contrast is most of what
       "designed for this viewport" actually feels like.

       Every clamp carries a rem INTERCEPT term. A bare vw preferred value
       (font-size: 4vw) stops text reaching 200% under browser zoom, which is
       a WCAG 1.4.4 failure; the rem term is exactly what keeps it passing,
       and a test pins that none of these lose it.

       Step count is locked to the ratio: a moderate ratio supports 8 to 10
       usable steps, and there are 9 below. The display step is deliberately
       NOT step 10 of the same ramp; it is a separate leap (42px to 96px) so
       there is a genuine display size rather than a long even ladder where
       three of the sizes are visually indistinguishable. ---- */
    --sf-fs-100: clamp(0.694rem, 0.692rem + 0.01vw, 0.7rem);
    --sf-fs-200: clamp(0.833rem, 0.816rem + 0.076vw, 0.875rem);
    --sf-fs-300: clamp(0.913rem, 0.886rem + 0.119vw, 0.978rem);
    --sf-fs-400: clamp(1rem, 0.962rem + 0.17vw, 1.094rem);
    --sf-fs-500: clamp(1.2rem, 1.132rem + 0.304vw, 1.367rem);
    --sf-fs-600: clamp(1.44rem, 1.33rem + 0.489vw, 1.709rem);
    --sf-fs-700: clamp(1.728rem, 1.561rem + 0.742vw, 2.136rem);
    --sf-fs-800: calc(clamp(2.074rem, 1.83rem + 1.085vw, 2.67rem) * var(--sf-display-scale, 1));
    --sf-fs-900: calc(clamp(2.488rem, 2.141rem + 1.545vw, 3.338rem) * var(--sf-display-scale, 1));
    --sf-fs-1000: calc(clamp(2.75rem, 0.5rem + 10vw, 8.25rem) * var(--sf-display-scale, 1));

    /* ---- line height, computed as part of the scale rather than hand-set
       per element. lh = 0.9 + 9.5/size_px, clamped to [1.0, 1.7], evaluated
       at each step's midpoint. One line-height for a caption and a headline
       is a template tell; this makes leading a function of size, which is
       what it physically is. ---- */
    --sf-lh-100: 1.7;
    --sf-lh-200: 1.6;
    --sf-lh-300: 1.53;
    --sf-lh-400: 1.47;
    --sf-lh-500: 1.36;
    --sf-lh-600: 1.28;
    --sf-lh-700: 1.21;
    --sf-lh-800: 1.15;
    --sf-lh-900: 1.1;
    --sf-lh-1000: 1.04;

    /* ---- tracking, derived from the shipped face at each step's midpoint.
       Every grotesque is spaced for about 16px and looks visibly gappy at
       four times that, so letter-spacing: normal on a 70px headline is the
       single most reliable template tell there is. These lean to the tighter
       end of the range because "precision instrument" is the brief; the
       per-theme --sf-track-offset dials the whole ramp back for the serif
       themes and positive for the uppercase one.

       All-caps needs POSITIVE tracking, which is what --sf-track-caps is
       for; it is never applied to lowercase. ---- */
    --sf-track-100: 0.03em;
    --sf-track-200: 0.005em;
    --sf-track-300: -0.006em;
    --sf-track-400: -0.008em;
    --sf-track-500: -0.014em;
    --sf-track-600: -0.018em;
    --sf-track-700: -0.022em;
    --sf-track-800: -0.026em;
    --sf-track-900: -0.03em;
    --sf-track-1000: -0.034em;

    /* ---- measure ----

       These numbers are MEASURED, not copied. 1ch is the advance of the '0'
       glyph, which in Archivo is 1.286x the frequency-weighted mean character
       advance of English prose, so a max-width of 68ch (what this file used
       to carry) renders about 87 characters per line, well past the readable
       band. 51ch is a true 66-character measure in this face. ---- */
    --sf-measure: var(--sf-measure-token, 51ch);
    --sf-measure-narrow: 39ch;
    --sf-measure-wide: 58ch;

    /* ---- spacing scale ----

       Eleven steps, every one at least 33% from its neighbour. Refactoring
       UI's threshold is 25%: below that a distinction is simply invisible,
       and a stylesheet carrying 16/18/20/22 and 24/26/28 has no rhythm, only
       twenty arbitrary numbers. That was this codebase before this pass. ---- */
    --sf-space-1: 4px;
    --sf-space-2: 8px;
    --sf-space-3: 12px;
    --sf-space-4: 16px;
    --sf-space-5: 24px;
    --sf-space-6: 32px;
    --sf-space-7: 48px;
    --sf-space-8: 64px;
    --sf-space-9: 96px;
    --sf-space-10: 128px;
    --sf-space-11: 176px;

    /* ---- section rhythm: FIVE distinct values, not one and not twenty ----

       The count of distinct section paddings is the best single predictor of
       perceived quality: a template ships one, a premium product ships five
       to seven. The magnitudes are calibrated against production sites at
       1440px, where the working part of the ramp (compact, standard,
       spacious) sits at 8 to 11 percent of content width and tight and
       chapter bookend it (this file previously sat at one value, 5.3%, which
       is closer to template than to premium).

       ONE PAIR, FIVE MULTIPLES, and that is the fix rather than a tidy-up.
       Five independently authored clamp() pairs are five independently
       authored SLOPES, and slopes that differ cross: this ramp was
       non-monotonic at desktop widths, where compact (112px) overtook
       standard (96px) at 1440 while still being smaller than it at 640. A
       ramp that reorders itself halfway across the viewport range cannot read
       as composed no matter what the individual numbers are, because it is
       not one ramp. Deriving every step from a single pair makes the order a
       property of the multipliers, which is checkable, instead of a property
       of where you happen to be looking.

       Whitespace scales SUBLINEARLY with viewport and LINEARLY with type, so
       the pair is a wide Utopia-style one rather than a steeper curve; the
       "luxurious on desktop" feel comes from assigning a wider pair to
       structural gaps, not from a steeper slope. Every step is multiplied by
       the theme's --sf-rhythm, which is how heritage reads generous and
       tactical reads dense without a second stylesheet. ---- */
    --sf-section-step: clamp(3.5rem, 0.7rem + 7vw, 9rem);

    --sf-pad-tight: calc(var(--sf-section-step) * 0.68 * var(--sf-rhythm, 1));
    --sf-pad-compact: calc(var(--sf-section-step) * 0.86 * var(--sf-rhythm, 1));
    --sf-pad-standard: calc(var(--sf-section-step) * 1 * var(--sf-rhythm, 1));
    --sf-pad-spacious: calc(var(--sf-section-step) * 1.14 * var(--sf-rhythm, 1));
    --sf-pad-chapter: calc(var(--sf-section-step) * 1.36 * var(--sf-rhythm, 1));

    /* Component padding is a ONE-UP PAIR (18px to 30px, x1.67 across the
       range) rather than a single step (which would travel x1.11 and be
       nearly static). Pairs are the slope control in this system. */
    --sf-card-pad: calc(clamp(1.125rem, 0.818rem + 1.364vw, 1.875rem) * var(--sf-density, 1));
    --sf-card-gap: calc(clamp(0.875rem, 0.733rem + 0.63vw, 1.25rem) * var(--sf-density, 1));

    /* Grid gap stays proportionally identical across the range: measured on
       production editorial sites, grid gap scales x1.00 from 1440 to 2560
       while type and section padding scale x1.40. */
    --sf-grid-gap: clamp(1rem, 0.85rem + 0.68vw, 1.5rem);

    /* ---- gutter: FIXED, with fluid columns ----

       Two philosophies exist and mixing them is the mistake. This is the
       editorial, print-derived one: the gutter is pinned and the gutter to
       column ratio falls as the viewport widens. Set in steps below rather
       than as a vw so it genuinely stops growing. ---- */
    --sf-gutter: 20px;

    --sf-stack: calc(clamp(1.75rem, 1.24rem + 2.27vw, 3.5rem) * var(--sf-rhythm, 1));

    /* ---- radius ramp, derived so a tenant overriding --sf-radius moves
       the whole scale coherently instead of one corner ---- */
    --sf-radius-xs: calc(var(--sf-radius) * 0.34);
    --sf-radius-lg: calc(var(--sf-radius) * 1.5);
    --sf-radius-xl: calc(var(--sf-radius) * 2.2);
    --sf-radius-pill: 999px;

    /* ---- aspect ratios: a SMALL CLOSED SET, one per CONTENT CLASS ----

       Mixed uncontrolled ratios are the single biggest reason merchant
       uploads wreck a good layout, and our dealers upload phone photos at
       arbitrary dimensions alongside distributor images at others. Four
       ratios site-wide, enforced on the MODULE, so a bad upload physically
       cannot change the grid.

       A closed set is not the same thing as ONE ratio, which is what this
       had become: product and square were both 1/1, so every merchandise
       slot on the platform was square regardless of what went in it. A
       merchandise tile CONTAINS its worst cases (cutouts, panoramas, vector
       specimen art) rather than cropping them, and a contained frame can
       only be as large as the worse of its two axis agreements. Against a
       square tile a 4:3 frame letterboxes by 12.5% of the tile height per
       side before any inset at all, and no inset value brings both axes into
       the 8 to 12% band; against 4/3 the same frame lands at 10.7% on both.
       That is why merchandise is landscape and the gallery thumb, which
       crops, stays square. See MediaFit.ProductTileRatio, which reasons about
       the same number on the renderer side and is held to this one by a
       test. ---- */
    --sf-ratio-product: 4 / 3;
    --sf-ratio-media: 3 / 2;
    --sf-ratio-square: 1 / 1;
    --sf-ratio-band: 21 / 9;
    --sf-ratio-video: 16 / 9;

    /* ---- the optical bounding box ----

       object-fit normalises BOUNDING BOXES, not perceived size. A rifle is
       roughly 4:1 and thin; a red dot is small and chunky. Scale both to fill
       the same tile and the optic ends up looking physically larger than the
       rifle, which is categorically wrong and is most of why a firearms grid
       reads amateur. --sf-optical is the correction, seeded per product from
       its category and exposed to the dealer as a slider. transform: scale is
       compositor-only, so it costs nothing. */
    --sf-optical: 1;

    /* Untrained photographers centre the subject in the viewfinder and
       include too much floor, so the interesting part of a merchant photo
       sits above the middle. */
    --sf-focal: 50% 42%;

    /* ---- elevation ramp. Built from the theme's shadow hue and strength
       so a cream theme casts a warm brown shadow and a near-black one
       casts a true black; a single shared shadow is why every card on
       this platform looked pasted on. ---- */
    --sf-elev-0: none;

    /* Layer one is always the CONTACT shadow: sub-pixel offset, sub-pixel
       blur, zero spread. It is what makes an element look like it is touching
       the surface rather than floating over it, and one-layer shadows can
       only ever be the ambient half. Every layer after it gets larger
       geometry AND larger NEGATIVE spread, which is what produces the falloff;
       alpha stays roughly constant across layers, contradicting the folk rule
       that alpha should decay. Blur equals the y-offset, one light-source
       ratio everywhere, and never pure black: the hue comes from the theme so
       a cream theme casts a warm brown shadow and the warm near-black casts a
       warm one. Capped at three layers because these repeat forty times in a
       product grid. */
    --sf-elev-1:
        0 0.5px 0.7px rgb(var(--sf-shadow-rgb) / calc(0.22 * var(--sf-shadow-strength)));
    --sf-elev-2:
        0 0.5px 0.7px rgb(var(--sf-shadow-rgb) / calc(0.2 * var(--sf-shadow-strength))),
        0 3px 3px -1.5px rgb(var(--sf-shadow-rgb) / calc(0.2 * var(--sf-shadow-strength))),
        0 8px 8px -4px rgb(var(--sf-shadow-rgb) / calc(0.2 * var(--sf-shadow-strength)));
    --sf-elev-3:
        0 0.5px 0.7px rgb(var(--sf-shadow-rgb) / calc(0.18 * var(--sf-shadow-strength))),
        0 7px 7px -2.5px rgb(var(--sf-shadow-rgb) / calc(0.18 * var(--sf-shadow-strength))),
        0 18px 18px -6px rgb(var(--sf-shadow-rgb) / calc(0.18 * var(--sf-shadow-strength)));
    --sf-elev-4:
        0 0.5px 0.7px rgb(var(--sf-shadow-rgb) / calc(0.16 * var(--sf-shadow-strength))),
        0 12px 12px -4px rgb(var(--sf-shadow-rgb) / calc(0.16 * var(--sf-shadow-strength))),
        0 34px 34px -12px rgb(var(--sf-shadow-rgb) / calc(0.16 * var(--sf-shadow-strength)));
    --sf-shadow: var(--sf-elev-2);

    /* The 1px inset that turns a flat rectangle into a piece of material:
       a lit top edge and a shaded bottom one. This, plus the lightness
       ramp above, is what replaces border: 1px solid everywhere. */
    --sf-edge: inset 0 1px 0 var(--sf-edge-highlight);
    --sf-edge-full:
        inset 0 1px 0 var(--sf-edge-highlight),
        inset 0 -1px 0 var(--sf-edge-shade);

    /* ---- motion ---- */
    --sf-dur-1: 120ms;
    --sf-dur-2: 200ms;
    --sf-dur-3: 320ms;
    --sf-dur-4: 560ms;
    --sf-dur-5: 900ms;
    /* Never ease-in on an entrance: it starts slow, which delays exactly the
       moment the reader is watching, so an ease-out at 200ms feels faster
       than an ease-in at the same duration. --sf-ease-signature is the
       theme's own curve and is what every interactive transition uses. */
    --sf-ease-out: var(--sf-ease-signature, cubic-bezier(0.23, 1, 0.32, 1));
    --sf-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
    --sf-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
    --sf-reveal-shift: 20px;

    /* ---- layout ----

       ONE cap, not two. Two simultaneous max-widths is the exact failure
       that makes a page read assembled rather than designed: section edges
       stop lining up vertically and the eye catches it even when the reader
       cannot say why. --sf-container is the single cap; the narrow and wide
       values below are TIERS OF THE SAME GRID (see .sf-canvas), not rival
       caps. The band is 1280-1360 on modern production sites; 1200 now reads
       slightly dated. ---- */
    --sf-container-narrow: 760px;
    --sf-container-wide: 1440px;
    --sf-header-height: 68px;

    /* ---- texture. A single tiling fractal-noise tile, painted UNDER the
       content as a background layer rather than over the viewport as a
       blended overlay: a fixed full-viewport mix-blend-mode layer forces
       the whole page to recomposite on every scroll frame, which is a
       real INP cost for a decorative effect. CSP allows data: under
       img-src, which is where CSS background images are checked. ---- */
    --sf-grain:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.06'/%3E%3C/svg%3E");
}

/* The fixed-gutter steps. Pinned values, so the gutter-to-column ratio falls
   as the viewport widens (the editorial choice) rather than staying locked
   (the proportional one). Mixing the two is the mistake. */
@media (min-width: 860px) {
    :root { --sf-gutter: 40px; }
}

@media (min-width: 1440px) {
    :root { --sf-gutter: 56px; }
}

/* ---------- reset ---------- */

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

* {
    margin: 0;
}

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    scroll-padding-top: calc(var(--sf-header-height) + var(--sf-space-4));
}

body {
    font-family: var(--sf-font-body);
    background-color: var(--sf-bg);
    background-image: var(--sf-grain);
    background-repeat: repeat;
    color: var(--sf-text);
    font-size: var(--sf-fs-400);
    font-weight: var(--sf-body-weight);
    line-height: var(--sf-lh-400);
    letter-spacing: calc(var(--sf-track-400) + var(--sf-track-offset));
    min-height: 100vh;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    overflow-wrap: break-word;

    /* font-synthesis defaults to ALL FOUR synthesis modes on, which silently
       produces faux bold on a face whose weight axis already covers it, and
       anaemic fake small caps on a face with no smcp table. Only style
       synthesis is kept, because we ship no italic file and a faux oblique is
       better than losing emphasis entirely. */
    font-synthesis: style;
}

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

input, button, textarea, select {
    font: inherit;
}

/* ---------- typography ---------- */

h1, h2, h3, h4, h5, h6 {
    font-family: var(--sf-font-heading);
    font-weight: var(--sf-heading-weight);
    text-transform: var(--sf-heading-transform);
    color: var(--sf-ink);
}

/* Size, line-height and tracking move together, taken from the same step of
   the scale. Weight barely moves between levels on purpose. */
h1 {
    font-size: var(--sf-fs-900);
    line-height: var(--sf-lh-900);
    letter-spacing: calc(var(--sf-track-900) + var(--sf-track-offset-display));
    font-weight: var(--sf-display-weight);
    text-transform: var(--sf-display-transform);
}

h2 {
    font-size: var(--sf-fs-800);
    line-height: var(--sf-lh-800);
    letter-spacing: calc(var(--sf-track-800) + var(--sf-track-offset));
}

h3 {
    font-size: var(--sf-fs-700);
    line-height: var(--sf-lh-700);
    letter-spacing: calc(var(--sf-track-700) + var(--sf-track-offset));
}

h4 {
    font-size: var(--sf-fs-600);
    line-height: var(--sf-lh-600);
    letter-spacing: calc(var(--sf-track-600) + var(--sf-track-offset));
}

h5, h6 {
    font-size: var(--sf-fs-500);
    line-height: var(--sf-lh-500);
    letter-spacing: calc(var(--sf-track-500) + var(--sf-track-offset));
}

/* balance evens the rag on short headings, which is one of the highest-signal
   single declarations available for a 2-3 line hero. It is deliberately NOT
   on body copy: it no-ops above six wrapped lines in Chromium, evens the rag
   too mechanically where it does fire, and Chrome warns about the cost of
   applying it broadly. pretty handles the widow on running text instead. */
h1, h2, h3, blockquote, figcaption, .sf-eyebrow, .sf-hero-heading, .sf-page-title {
    text-wrap: balance;
}

p, li, dd {
    text-wrap: pretty;
}

/* A heading at a tight line-height carries several percent of its font-size
   in dead half-leading above the cap line, so every heading in a card sits
   optically low and equal padding above and below looks unequal. Pure
   progressive enhancement: without support the heading is simply a little
   low, which is where it has always been. */
@supports (text-box: trim-both cap alphabetic) {
    h1, h2, h3, h4, .sf-btn, .sf-eyebrow, .sf-hero-heading, .sf-page-title {
        text-box: trim-both cap alphabetic;
    }
}

p {
    color: var(--sf-text);
}

a {
    color: var(--sf-link);
    text-decoration: none;
    text-underline-offset: 0.18em;
    text-decoration-thickness: from-font;
}

a:hover {
    text-decoration: underline;
}

ul, ol {
    padding-left: 1.4rem;
}

/* Numerals that change value or sit in a column are tabular, so digits stop
   jittering between states and prices line up down a grid. */
.sf-num,
.sf-product-price,
.sf-pdp-price,
.sf-fee-price,
.sf-facet-count,
.sf-service-price,
.sf-cart-line-price,
.sf-order-total,
.sf-hours-table td,
.sf-spec-row span:last-child,
.sf-footer-hours dd,
.sf-class-price,
.sf-range-rate-price,
.sf-range-fact-value {
    font-variant-numeric: tabular-nums;
}

/* Identifiers get a slashed zero so O and 0 cannot be confused when a
   shopper reads an order number down the phone. Never on prose. */
.sf-mono,
.sf-ffl-number,
.sf-order-number,
.sf-sku,
.sf-class-date,
.sf-classes-count,
.sf-class-meta,
.sf-staff-role,
.sf-staff-credentials,
.sf-financing-provider,
.sf-logostrip-label {
    font-family: var(--sf-font-mono);
    font-variant-numeric: tabular-nums slashed-zero;
    letter-spacing: 0.02em;
}

/* Two-tone focus ring. The box-shadow paints a solid dark band from 0 to 6px
   outside the border box; the outline paints the light band on top of it from
   2 to 4px. The result is dark / light / dark on every surface we have,
   including a brand-coloured button and a photo. */
:focus-visible {
    outline: 2px solid var(--sf-focus);
    outline-offset: 2px;
    box-shadow: 0 0 0 6px var(--sf-focus-contrast);
}

/* ================================================================
   layout primitives
   ================================================================ */

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

.sf-container-narrow { max-width: var(--sf-container-narrow); }
.sf-container-wide { max-width: var(--sf-container-wide); }

.sf-container-full {
    width: 100%;
}

/* ---------- the four-tier breakout grid ----------

   One grid, four widths, and the tier is the hierarchy: importance expressed
   purely as measure. This is what the block Container prop maps onto, which
   means a dealer picks Text / Wide / Feature / Full bleed and cannot produce
   a broken width, because there are only four and they are all tracks of the
   same grid.

   minmax(0, ...) on the popout and feature tracks is what makes them collapse
   to zero on a narrow viewport, so four tiers degrade to two with no media
   query at all. Naming a line pair x-start / x-end implicitly creates an area
   called x, so `grid-column: feature` just works.

   Full bleed is a grid track, NOT the old width:100vw / margin-left:-50vw
   hack: 100vw includes the scrollbar on Windows Chrome and Firefox, so that
   hack guarantees a horizontal scrollbar on the majority of our traffic. */
.sf-canvas {
    --sf-canvas-gutter: var(--sf-gutter);
    --sf-canvas-content: min(var(--sf-container-narrow), 100% - var(--sf-canvas-gutter) * 2);
    --sf-canvas-popout: minmax(0, 5rem);
    --sf-canvas-feature: minmax(0, 12rem);
    --sf-canvas-edge: minmax(var(--sf-canvas-gutter), 1fr);

    display: grid;
    grid-template-columns:
        [full-start] var(--sf-canvas-edge)
        [feature-start] var(--sf-canvas-feature)
        [popout-start] var(--sf-canvas-popout)
        [content-start] var(--sf-canvas-content) [content-end]
        var(--sf-canvas-popout) [popout-end]
        var(--sf-canvas-feature) [feature-end]
        var(--sf-canvas-edge) [full-end];
}

.sf-canvas > * { grid-column: content; }
.sf-tier-content { grid-column: content; }
.sf-tier-popout { grid-column: popout; }
.sf-tier-feature { grid-column: feature; }
.sf-tier-full { grid-column: full; }

/* ---------- grid overlap ----------

   Layering without position:absolute, so the container still sizes to its
   tallest child. Used for hero text over media. */
.sf-stackgrid { display: grid; }
.sf-stackgrid > * { grid-area: 1 / 1; }

/* ---------- flow spacing ----------

   The 1em fallback makes flow spacing relative to each element's OWN font
   size, so a large heading automatically gets more air than a caption
   without a single extra rule. lh where supported, so the rhythm is a
   multiple of the line box rather than of the em. */
.sf-flow > * + * { margin-block-start: var(--sf-flow-space, 1em); }

@supports (margin-block-start: 1lh) {
    .sf-flow > * + * { margin-block-start: var(--sf-flow-space, 0.75lh); }
}

/* ---------- section rhythm, applied exactly once ----------

   The padding classes NAME a step; the section APPLIES it, and there is one
   padding-block declaration on the whole system. Five classes each writing
   their own padding is how the ramp came to have a step nobody could reach:
   .sf-pad-compact was wired to --sf-pad-tight, so every "compact" section on
   every tenant site rendered at the tight value and --sf-pad-compact had no
   consumer at all. A ramp with a dead rung in the middle is a ramp with four
   values pretending to be five, which is most of why the rhythm read
   accidental. Routing every step through one custom property also makes the
   seam arithmetic below expressible at all: a rule can halve a section's own
   step without knowing which step it is. */

.sf-section {
    position: relative;
    padding-block: var(--sf-section-pad, 0px);
}

.sf-bg-default { background: var(--sf-bg); }
.sf-bg-alt { background: var(--sf-surface-alt); }
.sf-bg-brand { background: var(--sf-brand); color: var(--sf-on-brand); }
.sf-bg-brand :is(p, li, .sf-logostrip-label, .sf-staff-role, .sf-staff-credentials, .sf-video-caption) { color: var(--sf-on-brand); }
.sf-bg-dark { background: var(--sf-dark-bg); }
.sf-bg-dark p, .sf-bg-dark li { color: var(--sf-dark-text); }
.sf-bg-dark h1, .sf-bg-dark h2, .sf-bg-dark h3 { color: var(--sf-dark-ink); }

.sf-pad-none { --sf-section-pad: 0px; }
.sf-pad-tight { --sf-section-pad: var(--sf-pad-tight); }
.sf-pad-compact { --sf-section-pad: var(--sf-pad-compact); }
.sf-pad-standard { --sf-section-pad: var(--sf-pad-standard); }
.sf-pad-spacious { --sf-section-pad: var(--sf-pad-spacious); }
.sf-pad-chapter { --sf-section-pad: var(--sf-pad-chapter); }

/* ---------- a seam is ONE step, not two ----------

   The defect this fixes: every section paid a full step at the top AND a full
   step at the bottom, so the boundary between two of them was always twice
   the value the theme chose. A page of correctly calibrated sections then
   renders with every gap out of band, and the gaps that hurt most are the
   ones nobody authored: a trust bar and the heading under it were 300px
   apart on the demo home page, which reads as a rendering fault rather than
   as air. Calibrating the steps could never fix that, because the number on
   screen was never one of the steps.

   Who pays for the seam depends on whether the GROUND changes across it.

   Same ground, no visible line: the lower section's step is the whole seam
   and the upper section pays nothing, so the reader sees one deliberate gap.

   Ground changes: there IS a line, and a colour band whose content starts on
   its own top edge looks broken, so the step is split across the boundary and
   each side gets half of its own. The total is still one step.

   Both are :has()-free in the split case and :has() only in the same-ground
   case; where :has() is missing the same-ground seam simply stays as it was,
   which is the safe degradation this file uses everywhere. */

main > .sf-section:has(+ .sf-section) {
    padding-block-end: calc(var(--sf-section-pad, 0px) / 2);
}

main > .sf-section + .sf-section {
    padding-block-start: calc(var(--sf-section-pad, 0px) / 2);
}

/* Same ground: the lower section carries the whole step. Listed per class
   rather than by attribute so the specificity matches the two rules above
   exactly and source order decides, which is what makes this an override
   rather than a fight. */
main > .sf-section.sf-bg-default:has(+ .sf-section.sf-bg-default),
main > .sf-section.sf-bg-alt:has(+ .sf-section.sf-bg-alt),
main > .sf-section.sf-bg-brand:has(+ .sf-section.sf-bg-brand),
main > .sf-section.sf-bg-dark:has(+ .sf-section.sf-bg-dark) {
    padding-block-end: 0;
}

main > .sf-section.sf-bg-default + .sf-section.sf-bg-default,
main > .sf-section.sf-bg-alt + .sf-section.sf-bg-alt,
main > .sf-section.sf-bg-brand + .sf-section.sf-bg-brand,
main > .sf-section.sf-bg-dark + .sf-section.sf-bg-dark {
    padding-block-start: var(--sf-section-pad, 0px);
}

.sf-block + .sf-block { margin-top: var(--sf-stack); }

.sf-section-columns {
    display: grid;
    gap: var(--sf-grid-gap);
}

@media (min-width: 860px) {
    .sf-section-columns { grid-template-columns: repeat(var(--sf-section-cols, 2), minmax(0, 1fr)); }
    .sf-section-columns .sf-block + .sf-block { margin-top: 0; }
}

/* ---------- section composition (ultraplan round 4) ----------

   The consumers the four-tier canvas grid waited two rounds for. A section
   whose document carries a Composition renders as .sf-section.sf-canvas with
   ONE tier child (.sf-tier-*) or one two-track split wrapper; the legacy
   null-composition band keeps the container markup untouched.

   The split is the law-1 instrument: every template is asymmetric BY
   CONSTRUCTION (7-5, 5-7, 8-4, 4-8), never 1fr 1fr, and the offsets are a
   single occupied track against a deliberate void. Below 860px everything
   stacks in source order, which is also tab order (WCAG 1.3.2). */

.sf-sectionsplit {
    display: grid;
    gap: var(--sf-grid-gap);
    align-items: start;
}

/* Stacked blocks inside one track keep the normal block rhythm; the track
   gap does the separating between tracks. */

@media (min-width: 860px) {
    .sf-split-7-5 { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
    .sf-split-5-7 { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
    .sf-split-8-4 { grid-template-columns: minmax(0, 8fr) minmax(0, 4fr); }
    .sf-split-4-8 { grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); }

    /* A single occupied track on an 8fr/4fr grid: the void is a track, so it
       holds its width honestly instead of being a margin pretending to. */
    .sf-offset-left { grid-template-columns: minmax(0, 8fr) minmax(0, 4fr); }
    .sf-offset-left > .sf-split-a { grid-column: 1; }
    .sf-offset-right { grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); }
    .sf-offset-right > .sf-split-a { grid-column: 2; }
}

/* Edge bleed: the whole split wrapper is promoted to the full tier (a grid
   track, NEVER 100vw or a negative margin - 100vw includes the scrollbar on
   Windows and guarantees a horizontal scrollbar), and the copy side carries
   padding restoring the container line, so the media flank touches the
   viewport edge while the text keeps its column. Percentages here resolve
   against the full-tier wrapper, which spans the viewport. */
.sf-bleed-left,
.sf-bleed-right {
    grid-column: full;
}

@media (min-width: 860px) {
    .sf-bleed-left { padding-inline-end: max(var(--sf-gutter), calc((100% - var(--sf-container)) / 2)); }
    .sf-bleed-right { padding-inline-start: max(var(--sf-gutter), calc((100% - var(--sf-container)) / 2)); }
}

/* Overlap: the next section rides up over this seam on its own layer. MARGIN,
   not padding, so the one-declaration section rhythm stays exactly one
   declaration; the depth reuses the rhythm ramp so it scales with the theme. */
.sf-overlap-shallow,
.sf-overlap-deep {
    position: relative;
    z-index: 2;
}

.sf-overlap-shallow { margin-block-start: calc(-1 * var(--sf-pad-compact)); }
.sf-overlap-deep { margin-block-start: calc(-1 * var(--sf-pad-spacious)); }

/* An overlapped section must not ALSO pay the seam half-step: the overlap is
   the seam. Each class is doubled to reach (0,4,1), tying the same-ground
   seam rules above so SOURCE ORDER decides - this rule must stay after them. */
main > .sf-section + .sf-section.sf-overlap-shallow.sf-overlap-shallow,
main > .sf-section + .sf-section.sf-overlap-deep.sf-overlap-deep {
    padding-block-start: 0;
}

/* ---------- headings shared by blocks ---------- */

.sf-block-heading {
    margin-bottom: var(--sf-space-5);
    max-width: 18ch;
}

/* The eyebrow: small, tracked-out caps in the display face. All-caps needs
   POSITIVE tracking, which is what --sf-track-caps carries; it is never
   applied to lowercase. */
.sf-eyebrow {
    display: block;
    font-family: var(--sf-font-heading);
    font-size: var(--sf-fs-200);
    line-height: var(--sf-lh-200);
    font-weight: 600;
    text-transform: var(--sf-eyebrow-transform);
    letter-spacing: var(--sf-track-caps);
    color: var(--sf-text-soft);
    margin-bottom: var(--sf-space-3);
}

.sf-eyebrow-brand { color: var(--sf-brand); }

/* An editorial section head. The 7:5 track ratio is deliberate: below about
   1.2:1 two tracks read as an accident and at 1:1 they read as a template.
   The heading is also deliberately NOT full width. */
.sf-section-head {
    display: grid;
    gap: var(--sf-space-4);
    padding-bottom: var(--sf-space-5);
    margin-bottom: var(--sf-space-7);
    border-bottom: 1px solid var(--sf-hairline);
}

.sf-section-head h2 { margin: 0; max-width: 16ch; }
.sf-section-head-aside { color: var(--sf-text-soft); max-width: var(--sf-measure-narrow); }

@media (min-width: 860px) {
    .sf-section-head {
        grid-template-columns: 7fr 5fr;
        align-items: end;
        gap: var(--sf-space-8);
    }
}

/* ---------- buttons ---------- */

.sf-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sf-space-2);
    padding: 13px 24px;
    min-height: 44px;
    border-radius: var(--sf-btn-radius);
    border: 1.5px solid transparent;
    font-family: var(--sf-font-heading);
    font-weight: 600;
    font-size: var(--sf-fs-300);
    line-height: 1.2;
    letter-spacing: calc(var(--sf-track-300) + 0.02em);
    cursor: pointer;
    text-decoration: none;
    transition:
        background-color var(--sf-dur-2) var(--sf-ease-out),
        border-color var(--sf-dur-2) var(--sf-ease-out),
        color var(--sf-dur-2) var(--sf-ease-out),
        box-shadow var(--sf-dur-2) var(--sf-ease-out),
        transform var(--sf-dur-2) var(--sf-ease-out);
}

.sf-btn:hover { text-decoration: none; }

/* The press is FAST (dur-1 down); the release rides the spring override in
   the appended ultraplan region back out. */
.sf-btn:active {
    transform: scale(0.97);
    transition-duration: var(--sf-dur-1);
}

.sf-btn-primary {
    background: var(--sf-brand);
    color: var(--sf-on-brand);
    box-shadow: var(--sf-elev-2);
}

.sf-btn-primary:hover {
    background: var(--sf-brand-strong);
    color: var(--sf-on-brand);
    box-shadow: var(--sf-elev-3);
}

.sf-btn-secondary {
    background: transparent;
    border-color: var(--sf-control-border);
    color: var(--sf-ink);
}

.sf-btn-secondary:hover {
    background: var(--sf-surface-alt);
    border-color: var(--sf-brand);
    color: var(--sf-ink);
}

.sf-btn-ghost {
    background: transparent;
    color: var(--sf-link);
    padding-inline: var(--sf-space-2);
    min-height: 0;
}

.sf-btn-oncolor {
    background: var(--sf-bg);
    color: var(--sf-ink);
    border-color: transparent;
}

.sf-btn-oncolor:hover {
    background: var(--sf-surface);
    color: var(--sf-ink);
}

/* A link that reads as a link without being a button. The optical nudge on
   the rule is the same trick a right-pointing glyph needs: the mark sits a
   hair left of where the eye expects it. */
.sf-link-arrow {
    display: inline-flex;
    align-items: center;
    gap: var(--sf-space-2);
    font-family: var(--sf-font-heading);
    font-weight: 600;
    font-size: var(--sf-fs-300);
    letter-spacing: calc(var(--sf-track-300) + 0.02em);
    color: var(--sf-link);
}

.sf-link-arrow::after {
    content: "";
    width: 18px;
    height: 1px;
    background: currentColor;
    transform: translateX(0.08em);
    transition: transform var(--sf-dur-2) var(--sf-ease-out);
}

.sf-link-arrow:hover { text-decoration: none; }
.sf-link-arrow:hover::after { transform: translateX(calc(0.08em + 5px)); }

/* ---------- shared primitives ---------- */

.sf-icon {
    width: 22px;
    height: 22px;
    flex-shrink: 0;
}

.sf-chip {
    display: inline-block;
    background: var(--sf-badge-bg);
    color: var(--sf-badge-ink);
    border-radius: var(--sf-radius-pill);
    padding: 6px 14px;
    font-size: var(--sf-fs-300);
}

.sf-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

/* ---------- material ----------

   A .sf-panel is the replacement for "1px border on a flat rectangle".
   It sits one lightness step above its ground, carries the lit top edge,
   and casts a shadow tinted with the theme's own shadow hue. */
.sf-panel {
    background: var(--sf-surface);
    border-radius: var(--sf-radius);
    box-shadow: var(--sf-edge), var(--sf-elev-2);
}

.sf-panel-raised {
    background: var(--sf-surface-raised);
    box-shadow: var(--sf-edge), var(--sf-elev-3);
}

.sf-panel-sunken {
    background: var(--sf-surface-sunken);
    border-radius: var(--sf-radius);
    box-shadow: inset 0 1px 3px rgb(var(--sf-shadow-rgb) / calc(0.4 * var(--sf-shadow-strength)));
}

/* A hairline that is a gradient rather than a hard rule, so it reads as a
   seam in the material instead of a drawn box. */
.sf-hairline {
    height: 1px;
    border: 0;
    background: linear-gradient(90deg, transparent, var(--sf-hairline) 15%, var(--sf-hairline) 85%, transparent);
}

/* ---------- empty states ----------

   Our tenants are gun stores with bad phone photos or none at all, so the
   empty state is not an edge case, it is the common case. It gets a
   considered treatment: a sunken plate, a technical grid, a display-face
   line. Never a dashed rectangle apologising for itself. */

.sf-empty-state {
    position: relative;
    overflow: hidden;
    background: var(--sf-surface-sunken);
    border-radius: var(--sf-radius);
    box-shadow: inset 0 0 0 1px var(--sf-hairline);
    padding: var(--sf-space-9) var(--sf-space-5);
    text-align: center;
    color: var(--sf-text-soft);
}

.sf-empty-state::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(var(--sf-hairline) 1px, transparent 1px),
        linear-gradient(90deg, var(--sf-hairline) 1px, transparent 1px);
    background-size: 32px 32px;
    opacity: 0.5;
    mask-image: radial-gradient(circle at 50% 50%, #000, transparent 72%);
}

.sf-empty-state > * { position: relative; }

.sf-empty-state-title {
    font-family: var(--sf-font-heading);
    font-size: var(--sf-fs-600);
    line-height: var(--sf-lh-600);
    letter-spacing: calc(var(--sf-track-600) + var(--sf-track-offset));
    font-weight: var(--sf-heading-weight);
    color: var(--sf-ink);
    margin-bottom: var(--sf-space-2);
}

.sf-empty-state-inline { color: var(--sf-text-soft); }

/* ---------- the specimen plate ----------

   The CSS-only stand-in for a product with no photograph. It is type-led on
   purpose: a dealer who has never uploaded an image still gets a considered,
   consistent object rather than a grey box with a camera icon. The initials
   come from a data attribute the card sets, the ground is a machined-looking
   radial, and the technical grid ties it to the empty states above.

   Sized in cqi against the card, so the same component is correct in a
   four-up grid, a two-up related row and a full-width PDP slot with no
   variant and no builder choice to expose to the dealer. cqi falls back to
   small-viewport units where no container exists, so it degrades quietly. */

.sf-specimen {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background:
        radial-gradient(120% 90% at 50% 0%, var(--sf-surface-alt), var(--sf-surface-sunken) 72%);
    color: var(--sf-text-faint);

    /* The plate has to read as a PLATE next to its siblings. Three cards in a
       row where two carry a framed image and the third is a slightly
       different shade of the card it sits on is three different media
       treatments, and the odd one out looks like a failed image load rather
       than like a deliberate stand-in. Its own ground is only ever a couple
       of lightness steps from --sf-surface, which is not enough separation to
       survive on its own, so the edge does the work: the same signature edge
       every other raised surface in this system carries, over a hairline so
       the plate has a boundary on a theme whose signature edge is faint. */
    border-radius: var(--sf-radius-xs);
    box-shadow: inset 0 0 0 1px var(--sf-hairline), var(--sf-signature-edge);
}

.sf-specimen::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(var(--sf-hairline) 1px, transparent 1px),
        linear-gradient(90deg, var(--sf-hairline) 1px, transparent 1px);
    background-size: 28px 28px;
    opacity: 0.55;
    mask-image: radial-gradient(circle at 50% 45%, #000, transparent 78%);
}

.sf-specimen-mark {
    position: relative;
    font-family: var(--sf-font-heading);
    font-weight: var(--sf-display-weight);
    font-size: clamp(2rem, 18cqi, 5rem);
    letter-spacing: var(--sf-track-1000);
    line-height: 1;
    color: var(--sf-ink);

    /* Was 0.16, which on a dark theme is ink at roughly 1.4:1 against its own
       ground: legible as a smudge and nothing more. The mark is the entire
       content of the plate, so if it is invisible the plate is an empty box
       and the card next to a photographed one looks broken. It is still
       deliberately quiet (it is a stand-in, not a headline) but it now has to
       actually be readable. Decorative by role: the element is aria-hidden
       and the product name is right underneath it, so this is a composition
       decision rather than a contrast-minimum one. */
    opacity: 0.34;
    text-transform: uppercase;
    user-select: none;
}

/* A hairline crosshair through the plate: reads as a datum mark on a
   drawing, and gives the empty tile something to be about. */
.sf-specimen::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 46%;
    height: 1px;
    transform: translate(-50%, -50%);
    background: linear-gradient(90deg, transparent, var(--sf-brand), transparent);
    opacity: 0.4;
}

.sf-dev-error {
    border: 2px solid var(--sf-critical);
    background: color-mix(in srgb, var(--sf-critical) 12%, transparent);
    border-radius: var(--sf-radius-sm);
    padding: 12px 16px;
    font-size: var(--sf-fs-300);
    display: flex;
    flex-direction: column;
    gap: 4px;
}

/* ---------- prose (rich text) ---------- */

.sf-prose, .sf-prose-narrow, .sf-prose-wide {
    max-width: var(--sf-measure);
}

.sf-prose-narrow { max-width: var(--sf-measure-narrow); }
.sf-prose-wide { max-width: 100%; }

.sf-richtext > * + * { margin-top: 0.9em; }

@supports (margin-top: 1lh) {
    .sf-richtext > * + * { margin-top: 0.7lh; }
}

.sf-richtext h2 { margin-top: 1.4em; }
.sf-richtext h2:first-child { margin-top: 0; }

.sf-richtext blockquote {
    border-left: 3px solid var(--sf-brand);
    padding-left: var(--sf-space-4);
    color: var(--sf-text-soft);
    font-style: italic;
}

.sf-richtext li { color: var(--sf-text); }

/* ================================================================
   motion

   Zero JavaScript. Scroll-driven reveals use animation-timeline: view(),
   which is driven by the compositor and needs no IntersectionObserver, and
   they are declared INSIDE an @supports block so a browser without it simply
   renders the content, already visible, with no animation. That is the
   required degradation: nothing on this site becomes visible BECAUSE of an
   animation, so a reveal that never runs costs the reader nothing.

   Only transform and opacity are animated anywhere in these five files.
   ================================================================ */

/* THE @supports CONDITION IS LOAD BEARING AND MUST TEST animation-range TOO.
   Firefox ships the scroll timeline WITHOUT the range property, so a
   timeline-only test passes there and then the keyframes run against a
   default range: the element keeps the `from` state, opacity 0, permanently,
   while staying in the accessibility tree. Content that is invisible but
   announced is the worst possible outcome. Testing both properties is what
   makes the degradation "no animation, content visible".

   The finished state is the BASE rule and the hidden state exists only inside
   the keyframes, so an engine that never runs the animation renders the page
   already visible. And animation-timeline is declared AFTER the animation
   shorthand, because the shorthand resets it to auto. */
.sf-reveal { opacity: 1; transform: none; }

@media (prefers-reduced-motion: no-preference) {
    @supports (animation-timeline: view()) and (animation-range: entry 5%) {
        .sf-reveal {
            animation: sf-reveal-in linear both;
            animation-timeline: view();
            animation-range: entry 5% cover 26%;
        }

        /* The hero settles as it leaves rather than as it enters: it is
           already on screen at load, so an entry animation would either
           flash or do nothing. */
        .sf-hero-parallax {
            animation: sf-hero-settle linear both;
            animation-timeline: view();
            animation-range: exit -10% exit 100%;
        }
    }
}

@keyframes sf-reveal-in {
    from {
        opacity: 0;
        transform: translateY(var(--sf-reveal-shift)) scale(0.995);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

@keyframes sf-hero-settle {
    from { transform: none; opacity: 1; }
    to { transform: translateY(-6%) scale(1.04); opacity: 0.55; }
}

/* Cross-document view transitions: a same-origin navigation cross-fades
   instead of flashing white. Zero JavaScript, and inert where unsupported.

   GATED TO DESKTOP ON PURPOSE. The transition costs roughly 70ms of mobile
   LCP because the outgoing snapshot has to be taken before the new document
   paints, and this platform is sold on Core Web Vitals: a nicer navigation
   that costs the headline metric on the majority of our traffic is a
   downgrade, not a polish item. Desktop has the headroom, phones do not. */
@media (prefers-reduced-motion: no-preference) and (min-width: 900px) {
    @view-transition {
        navigation: auto;
    }
}

/* Below-the-fold sections skip layout and paint until they are near the
   viewport. `auto` in contain-intrinsic-size makes the browser remember the
   real size once measured, so the scrollbar does not jump and CLS stays at
   zero. The first two sections are excluded: they are the LCP path. */
@supports (content-visibility: auto) {
    main > .sf-section:nth-of-type(n + 3) {
        content-visibility: auto;
        contain-intrinsic-size: auto 520px;
    }
}

/* ---------- reduced motion ---------- */

/* Motion is decoration everywhere in this renderer, so under a reduced-motion
   preference it all goes: transitions, scroll-driven reveals, the smooth
   scroll that hijacks every in-page jump, the hover lifts (which unlike a
   transition PERSIST), and the cross-document view transition. Nothing here
   is load-bearing for content, so a reduced-motion reader loses nothing at
   all. The reveal keyframes are already scoped to a no-preference media
   query above, so they never start in the first place. */
@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        transition-delay: 0ms !important;
        scroll-behavior: auto !important;
        animation-timeline: none !important;
    }

    /* Hover lifts are motion too, and unlike a transition they persist. */
    .sf-product-card:hover,
    .sf-brand-tile:hover,
    .sf-category-tile:hover,
    .sf-feature:hover,
    .sf-gallery-item:hover img,
    .sf-specimen:hover {
        transform: none;
    }

    ::view-transition-group(*),
    ::view-transition-old(*),
    ::view-transition-new(*) {
        animation: none !important;
    }
}

/* ================================================================
   surface, texture and imagery primitives
   Appended, never inserted (cascade order).
   ================================================================ */

/* color-scheme is a COMPUTED property, not a selector, so every light-dark()
   value and every UA-rendered control (scrollbars, form widgets, the canvas
   behind a short page) keys off it. A cream theme that does not declare it
   gets dark scrollbars and dark form controls on paper. */
:root {
    color-scheme: var(--sf-color-scheme, dark);
}

/* iOS paints a grey box over any tapped link unless this is cleared, which is
   the single most visible "not a real app" tell on a phone. */
html {
    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

/* The grain is blended into the background PAINT rather than stacked as a
   scrolling layer. A fixed full-viewport mix-blend-mode overlay forces the
   whole page to recomposite every scroll frame; background-blend-mode is one
   paint and costs nothing after it. The data URI is an IMAGE, not a live
   filter, so the turbulence is rasterised once and cached: it never re-runs
   on scroll, which `filter: url(#noise)` on a live element would. */
body {
    background-blend-mode: var(--sf-grain-blend, overlay);
}

/* The same tile doubles as a gradient dither. Banding is 8-bit quantisation,
   not a colour-space problem, so working in oklab does not fix it; one
   8-bit step of noise does. A hero gradient spanning 600px between two close
   tones bands visibly without this. */
.sf-hero-gradient,
.sf-ctaband,
.sf-map-frame {
    background-image: var(--sf-grain);
    background-blend-mode: var(--sf-grain-blend, overlay);
}

/* Two-colour brand gradients interpolate through mud in sRGB. Guarded,
   because an unrecognised interpolation keyword invalidates the whole
   gradient and would leave the hero with no background at all. */
@supports (background: linear-gradient(in oklab, red, blue)) {
    .sf-hero-gradient {
        background:
            radial-gradient(120% 130% at 78% -20%, var(--sf-hero-grad-b), transparent 62%),
            linear-gradient(158deg in oklab, var(--sf-hero-grad-a), var(--sf-hero-grad-b));
    }
}

/* ---------- the signature edge ----------

   Applied to every raised surface and every section seam. This is the token
   that gives a site built on this platform a fingerprint instead of the
   median output every default kit produces, and it differs per theme. */
.sf-panel,
.sf-panel-raised,
.sf-product-card,
.sf-facet-rail,
.sf-ffl-card,
.sf-what-to-bring,
.sf-nav {
    box-shadow: var(--sf-signature-edge), var(--sf-elev-2);
}

.sf-panel-raised,
.sf-nav {
    box-shadow: var(--sf-signature-edge), var(--sf-elev-4);
}

/* ---------- the tone normaliser ----------

   Merchant photography arrives at four white balances and three exposures on
   the same page. `mix-blend-mode: color` takes hue and saturation from the
   overlay and LUMINOSITY from the photograph underneath, which produces a
   true monotone in one declaration on a colour photo with no grayscale step.
   The opacity is ONE dealer-facing slider from untouched to full monotone.

   Two things this needs or it silently does nothing: `isolation: isolate` on
   the wrapper, or it blends against everything in the ancestor stacking
   context, and an OPAQUE wrapper background, because a blend mode has nothing
   to composite against on a transparent one. It also has to be mix-blend-mode
   on a pseudo-element: background-blend-mode never touches the replaced
   content of an <img>, which is the most common way this is got wrong. */
.sf-tint {
    position: relative;
    isolation: isolate;
    background: var(--sf-surface-sunken);
}

.sf-tint::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(150deg, var(--sf-hero-grad-a), var(--sf-hero-grad-b));
    mix-blend-mode: color;
    opacity: var(--sf-tint-strength, 0);
    pointer-events: none;
}

/* ---------- the drafting grid ----------

   64px minor, 320px major, at the opacity where it registers as paper rather
   than as a chart. The half-offset major rule is what stops it reading as
   graph paper. */
.sf-drafting {
    position: relative;
}

.sf-drafting::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image:
        linear-gradient(var(--sf-hairline) 1px, transparent 1px),
        linear-gradient(90deg, var(--sf-hairline) 1px, transparent 1px),
        linear-gradient(var(--sf-hairline) 1px, transparent 1px),
        linear-gradient(90deg, var(--sf-hairline) 1px, transparent 1px);
    background-size: 64px 64px, 64px 64px, 320px 320px, 320px 320px;
    opacity: 0.6;
}

/* Form controls must never be under 16px on iOS or Safari zooms the viewport
   the moment the field is focused, which reads as a bug to every shopper who
   has ever seen it. */
input, textarea, select {
    font-size: max(16px, var(--sf-fs-300));
}

/* Centred multi-line body copy is a named template tell. Short headings may
   be centred; running copy inside a centred block is left-aligned back. */
.sf-empty-state p,
.sf-notfound p {
    text-align: left;
    max-width: var(--sf-measure-narrow);
    margin-inline: auto;
}

/* Glass is for floating chrome only, and it must have an opaque fallback for
   readers who have asked for less transparency. */
@media (prefers-reduced-transparency: reduce) {
    .sf-header {
        background: var(--sf-header-bg);
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
    }
}

/* Forced-colors mode discards our whole palette, so the material system has
   to fall back to real borders rather than to nothing at all. */
@media (forced-colors: active) {
    .sf-panel,
    .sf-product-card,
    .sf-facet-rail,
    .sf-nav {
        border: 1px solid CanvasText;
    }
}

/* ================================================================
   the four width tiers

   Importance expressed purely as measure, and the thing the block Container
   prop maps onto: a dealer picks Text / Wide / Feature / Full bleed and
   cannot produce a broken width because there are only four and all four are
   DERIVED FROM THE ONE CAP. Two simultaneous max-widths is the exact failure
   that makes a page read assembled rather than designed, because section
   edges stop lining up vertically and the eye catches it even when the reader
   cannot say why. There is one cap here, --sf-container, and three fractions
   of it.

   Full bleed is width:100% inside a normal flow element, NOT the
   width:100vw / margin-left:-50vw hack: 100vw includes the scrollbar on
   Windows Chrome and Firefox, and that hack guarantees a horizontal
   scrollbar for most of our traffic.
   ================================================================ */

.sf-container-narrow {
    max-width: min(var(--sf-container-narrow), var(--sf-container));
}

.sf-container-wide {
    max-width: min(calc(var(--sf-container) * 1.12), var(--sf-container-wide));
}

/* ================================================================
   ultraplan round 4: word-as-graphic type, ghost numerals, spring
   motion, the media plate tokens.
   Appended, never inserted (cascade order).
   ================================================================ */

:root {
    /* Two steps ABOVE the display leap (creative spec section 0). fs-1100 is
       type as graphic: a word set so large it reads as image (the footer
       wordmark, hero typographic layouts). fs-numeral is the ghost-numeral
       step for stats and index ordinals. Both keep the mandatory rem
       intercept: a bare-vw preferred value stops text reaching 200% under
       browser zoom (WCAG 1.4.4), and a test holds every fluid step to it. */
    --sf-fs-1100: calc(clamp(4.5rem, 1.8rem + 12vw, 13rem) * var(--sf-display-scale, 1));
    --sf-lh-1100: 0.92;
    --sf-fs-numeral: clamp(3.5rem, 1.6rem + 8.5vw, 9rem);

    /* ---- motion: the spring ----

       A real damped spring sampled into linear() (the only way CSS gets an
       overshooting curve outside cubic-bezier's [0,1] y-range on transforms).
       Consumed behind @supports so an engine without linear() keeps the
       signature ease rather than losing the transition to an invalid
       computed value. */
    --sf-ease-spring: linear(0, .0087 1.06%, .0349 2.13%, .1394 4.4%, .5698 10.34%, .7825 13.63%, .8613 15.16%, .9284 16.77%, .9839 18.55%, 1.0257 20.55%, 1.0546 22.86%, 1.0693 25.66%, 1.0653 30.25%, 1.0164 41.08%, .998 50.8%, .9994 68.42%, 1);
    --sf-stagger: 45ms;
    --sf-stagger-range: 5%;
    --sf-marquee-speed: 42s;

    /* ---- the media plate + editorial ink (gunmetal FALLBACKS; the theme
       block overrides on html:root exactly like every token above). scrim
       and numeral-ink are decoration-role: the scrim raises the contrast of
       text over imagery (measured through the hero-ink pairs) and the ghost
       numeral is aria-hidden ornament, so neither carries its own AA pair;
       rule-accent does (ThemeTokens.ContrastPairs). ---- */
    --sf-plate: #F2EFE9;
    --sf-plate-edge: #00000014;
    --sf-plate-deep: #161310;
    --sf-plate-inset: 12px;
    --sf-scrim: linear-gradient(180deg, transparent 40%, rgba(6, 5, 4, 0.78));
    --sf-numeral-ink: rgba(201, 164, 92, 0.16);
    --sf-rule-accent: #C9A45C;
    --sf-outline-ink: rgba(244, 240, 232, 0.9);
    --sf-duotone: 0;
}

/* Numerals at display size are ALWAYS tabular and lining (law 5): digits
   that jitter in width or dip below the baseline cannot be a graphic. */
.sf-numeral {
    font-family: var(--sf-font-heading);
    font-size: var(--sf-fs-numeral);
    line-height: 1;
    font-variant-numeric: tabular-nums lining-nums;
}

/* The word-as-graphic step, packaged so consumers cannot forget the tight
   leading that makes it read as an object rather than a headline. */
.sf-word-graphic {
    font-family: var(--sf-font-heading);
    font-weight: var(--sf-display-weight);
    text-transform: var(--sf-display-transform);
    font-size: var(--sf-fs-1100);
    line-height: var(--sf-lh-1100);
    letter-spacing: calc(var(--sf-track-1000) + var(--sf-track-offset-display));
    white-space: nowrap;
}

/* ---------- button grammar (motion spec 2.2) ----------

   The press is a spring on transform only. Base .sf-btn keeps its full
   signature-ease transition list; this override swaps ONLY the transform
   term where linear() exists, so an engine without it degrades to the
   signature curve instead of to no transition at all (an invalid easing at
   computed-value time would unset the whole shorthand). */
@supports (transition-timing-function: linear(0, 1)) {
    .sf-btn {
        transition:
            background-color var(--sf-dur-2) var(--sf-ease-out),
            border-color var(--sf-dur-2) var(--sf-ease-out),
            color var(--sf-dur-2) var(--sf-ease-out),
            box-shadow var(--sf-dur-2) var(--sf-ease-out),
            transform var(--sf-dur-3) var(--sf-ease-spring);
    }
}

/* An arrow glyph inside a button nudges on hover/focus of its BUTTON, not of
   itself: the affordance belongs to the control. Transform only. */
.sf-btn-arrow {
    display: inline-block;
    transition: transform var(--sf-dur-2) var(--sf-ease-out);
}

.sf-btn:hover .sf-btn-arrow,
.sf-btn:focus-visible .sf-btn-arrow {
    transform: translateX(4px);
}

/* ---------- the center-out rule (motion spec 2.3) ----------

   The nav hover affordance, promoted to a shared utility so the footer links
   (and anything lane B adopts it for) draw the same mark: a rule growing
   from the centre, transform-only, visible on keyboard focus too. */
.sf-underline-center {
    position: relative;
}

.sf-underline-center::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    bottom: -2px;
    height: 1px;
    background: var(--sf-brand);
    transform: scaleX(0);
    transform-origin: center;
    transition: transform var(--sf-dur-2) var(--sf-ease-out);
}

.sf-underline-center:hover::after,
.sf-underline-center:focus-visible::after {
    transform: scaleX(1);
}

.sf-underline-center:hover {
    text-decoration: none;
}
