/* 02-shell.css: announcement bar, site header, navigation (pure-CSS mobile
   drawer via <details>), and footer. Styled entirely off --sf-* tokens.

   The shell is where a site is judged in the first 200ms, and the old one was
   a flat bar with a 1px border under it. This one is a piece of material: a
   translucent, blurred plate that sits above the page, seamed to the content
   with a gradient hairline rather than a drawn line. */

/* ---------- skip link (WCAG 2.4.1) ---------- */

/* Off screen until focused, then a real button pinned over the header. Not
   display:none and not visibility:hidden: both take it out of the focus order,
   which is the whole point of the control. */
.sf-skip-link {
    position: absolute;
    top: -100px;
    left: 12px;
    z-index: 60;
    background: var(--sf-brand);
    color: var(--sf-on-brand);
    font-weight: 700;
    padding: 12px 20px;
    border-radius: var(--sf-btn-radius);
    text-decoration: none;
}

.sf-skip-link:focus {
    top: 12px;
}

.sf-skip-link:hover { text-decoration: none; }

/* ---------- announcement ---------- */

/* The old announcement bar was a solid slab of brand colour across the top of
   every page, which is exactly what made a restrained brass accent read as a
   warning label. It is now the ground colour with a brand hairline and brand
   text: the same information, one tenth of the shouting, and the accent is
   still available to be spent once further down the page. */
.sf-announcement {
    background: var(--sf-surface-sunken);
    color: var(--sf-ink);
    text-align: center;
    padding: 9px 16px;
    font-size: var(--sf-fs-200);
    font-weight: 600;
    letter-spacing: 0.02em;
    box-shadow: inset 0 -1px 0 var(--sf-hairline);
}

.sf-announcement::before {
    content: "";
    display: inline-block;
    width: 6px;
    height: 6px;
    margin-right: 10px;
    border-radius: 50%;
    background: var(--sf-brand);
    vertical-align: middle;
}

/* ---------- header ---------- */

.sf-header {
    background: var(--sf-header-bg);
    position: sticky;
    top: 0;
    z-index: 40;

    /* A scroll-state query container: the condense-on-stick rules in the
       appended ultraplan region ask this element whether it is currently
       stuck to the top. Chromium 133+; everywhere else the declaration is
       dropped and the header simply keeps its full height. */
    container-type: scroll-state;
    container-name: sf-header;
}

/* The seam: a gradient hairline that fades out at both ends, rather than a
   1px rule ruled edge to edge. */
.sf-header::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    height: 1px;
    background: linear-gradient(
        90deg,
        transparent,
        var(--sf-header-border) 10%,
        var(--sf-header-border) 90%,
        transparent);
}

/* Translucent + blurred where supported, which is what makes content feel
   like it passes UNDER the header rather than being clipped by it. Guarded:
   without backdrop-filter the header stays fully opaque and nothing is lost. */
@supports (backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px)) {
    .sf-header {
        background: color-mix(in srgb, var(--sf-header-bg) 82%, transparent);
        -webkit-backdrop-filter: blur(16px) saturate(1.3);
        backdrop-filter: blur(16px) saturate(1.3);
    }
}

.sf-header-inner {
    display: flex;
    align-items: center;
    gap: var(--sf-space-4);
    min-height: var(--sf-header-height);
}

.sf-logo {
    display: flex;
    align-items: center;
    gap: 10px;
    font-family: var(--sf-font-heading);
    font-weight: var(--sf-heading-weight);
    text-transform: var(--sf-heading-transform);
    letter-spacing: calc(var(--sf-track-700) + var(--sf-track-offset));
    font-size: var(--sf-fs-600);
    color: var(--sf-header-ink);
    margin-right: auto;
    white-space: nowrap;
    line-height: 1;
}

/* The one brand mark in the shell: a short brass rule before the name. It is
   the "once per viewport" accent for the header, which frees the nav, the
   phone link and the cart to be quiet. */
.sf-logo::before {
    content: "";
    width: 3px;
    height: 1.05em;
    border-radius: 2px;
    background: var(--sf-brand);
    flex-shrink: 0;
}

.sf-logo:hover { text-decoration: none; color: var(--sf-brand); }

/* ---------- uploaded logo ---------- */

/* An uploaded logo replaces the name TEXT, never the link: the business name
   rides along as the img alt, so the accessible name of the home link is
   identical either way. Height-capped so the sticky header keeps its fixed
   min-height on every scroll position (a header that resizes is layout
   shift); the width cap stops a very wide wordmark crowding the nav. Both
   caps preserve the intrinsic aspect ratio from the width/height attributes,
   so the box is reserved before the file arrives (CLS ~0). */
.sf-logo-img {
    display: block;
    width: auto;
    height: auto;
    max-height: 40px;
    max-width: min(46vw, 240px);
}

/* The brass rule is the shell's one brand accent for TEXT wordmarks. With a
   real logo the mark itself is the accent, and a decorative tick beside it
   reads as clutter, so it stands down. */
.sf-logo-imaged::before { display: none; }

/* Footer logo: slightly taller cap than the header (the footer is a sign-off,
   not floating chrome), same ratio-preserving constraints. */
.sf-footer-logo {
    display: block;
    width: auto;
    height: auto;
    max-height: 52px;
    max-width: min(60vw, 220px);
}

.sf-footer-name-imaged { max-width: none; }

.sf-header-phone {
    display: none;
    align-items: center;
    gap: var(--sf-space-2);
    color: var(--sf-header-ink);
    font-weight: 600;
    font-size: var(--sf-fs-300);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.sf-header-phone .sf-icon { width: 17px; height: 17px; color: var(--sf-brand); }

/* ---------- nav (mobile drawer first) ---------- */

.sf-nav-drawer { position: relative; }

.sf-nav-drawer > summary {
    list-style: none;
    cursor: pointer;
    padding: 10px;
    border-radius: var(--sf-radius-sm);
    display: flex;
}

.sf-nav-drawer > summary::-webkit-details-marker { display: none; }

.sf-nav-burger {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 5px;
    width: 24px;
    height: 20px;
}

.sf-nav-burger span {
    height: 2px;
    background: var(--sf-header-ink);
    border-radius: 2px;
    transition: transform var(--sf-dur-2) var(--sf-ease-out), opacity var(--sf-dur-1) linear;
}

.sf-nav-drawer[open] > summary .sf-nav-burger span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.sf-nav-drawer[open] > summary .sf-nav-burger span:nth-child(2) { opacity: 0; }
.sf-nav-drawer[open] > summary .sf-nav-burger span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.sf-nav {
    position: absolute;
    right: 0;
    top: calc(100% + 10px);
    min-width: 232px;
    background: var(--sf-surface-raised);
    border-radius: var(--sf-radius);
    box-shadow: var(--sf-edge), var(--sf-elev-4);
    display: flex;
    flex-direction: column;
    padding: var(--sf-space-2);
    z-index: 50;
}

/* The drawer choreography (motion spec 5.2): the panel and its items
   transition in from @starting-style when the <details> opens, the items
   time-staggered down the index. An entry transition degrades to "appears
   instantly" everywhere @starting-style is missing, and the panel is never
   the only route to the content: the same links are in the footer. Base
   states are the FINISHED states, so nothing is ever invisible without it. */
@media (prefers-reduced-motion: no-preference) {
    .sf-nav-drawer > .sf-nav {
        transform-origin: top right;

        /* display rides along discretely so engines that render the closed
           <details> content as display:none still fire the entry transition;
           an engine without allow-discrete drops this whole declaration and
           the drawer simply appears, base state already visible. */
        transition:
            opacity var(--sf-dur-3) var(--sf-ease-drawer),
            transform var(--sf-dur-3) var(--sf-ease-drawer),
            display var(--sf-dur-3) allow-discrete;
    }

    .sf-nav-drawer > .sf-nav > * {
        transition:
            opacity var(--sf-dur-3) var(--sf-ease-drawer),
            transform var(--sf-dur-3) var(--sf-ease-drawer);
        transition-delay: calc(var(--sf-i, 0) * var(--sf-stagger, 45ms));
    }

    /* Six delays, then the stagger stops: a stagger longer than ~300ms reads
       as the interface lagging rather than choreography. */
    .sf-nav-drawer > .sf-nav > :nth-child(1) { --sf-i: 0; }
    .sf-nav-drawer > .sf-nav > :nth-child(2) { --sf-i: 1; }
    .sf-nav-drawer > .sf-nav > :nth-child(3) { --sf-i: 2; }
    .sf-nav-drawer > .sf-nav > :nth-child(4) { --sf-i: 3; }
    .sf-nav-drawer > .sf-nav > :nth-child(5) { --sf-i: 4; }
    .sf-nav-drawer > .sf-nav > :nth-child(n + 6) { --sf-i: 5; }

    @starting-style {
        .sf-nav-drawer[open] > .sf-nav {
            opacity: 0;
            transform: translateY(-8px) scale(0.98);
        }

        .sf-nav-drawer[open] > .sf-nav > * {
            opacity: 0;
            transform: translateY(-6px);
        }
    }
}

.sf-nav a {
    position: relative;
    color: var(--sf-header-ink);
    padding: 11px 14px;
    border-radius: var(--sf-radius-sm);
    font-family: var(--sf-font-heading);
    font-weight: 600;
    font-size: var(--sf-fs-300);
    letter-spacing: 0.02em;
}

.sf-nav a:hover {
    background: var(--sf-surface-alt);
    text-decoration: none;
}

.sf-nav a.sf-nav-active {
    color: var(--sf-brand);
}

/* Click-to-call link inside the mobile drawer only. */
.sf-nav-phone {
    display: flex;
    align-items: center;
    gap: var(--sf-space-2);
    color: var(--sf-header-ink);
    font-weight: 600;
    padding: 11px 14px;
    margin-top: var(--sf-space-1);
    border-radius: var(--sf-radius-sm);
    white-space: nowrap;
    box-shadow: inset 0 1px 0 var(--sf-hairline);
}

.sf-nav-phone .sf-icon { width: 16px; height: 16px; color: var(--sf-brand); }
.sf-nav-phone:hover { background: var(--sf-surface-alt); text-decoration: none; }

/* The inline (desktop) nav is hidden on mobile; the drawer serves there.
   A closed <details> never paints its children, so the desktop nav must
   live outside it (UX-blocker-2). */
.sf-nav-inline { display: none; }

@media (min-width: 860px) {
    .sf-header-phone { display: flex; }

    /* Whole drawer (summary AND its nav copy) leaves the tree on desktop;
       the inline nav takes over. */
    .sf-nav-drawer { display: none; }

    .sf-nav-inline {
        position: static;
        display: flex;
        flex-direction: row;
        background: transparent;
        box-shadow: none;
        padding: 0;
        gap: 2px;
        min-width: 0;
    }

    .sf-nav-inline a { padding: 8px 14px; }

    /* The hover affordance is a rule that grows from the centre, drawn with
       a scaled pseudo-element: transform only, no layout, no repaint of the
       text. */
    .sf-nav-inline a::after {
        content: "";
        position: absolute;
        left: 14px;
        right: 14px;
        bottom: 2px;
        height: 2px;
        background: var(--sf-brand);
        transform: scaleX(0);
        transform-origin: center;
        transition: transform var(--sf-dur-2) var(--sf-ease-out);
    }

    .sf-nav-inline a:hover::after,
    .sf-nav-inline a.sf-nav-active::after { transform: scaleX(1); }

    .sf-nav-inline a:hover { background: transparent; }
}

/* ---------- footer ---------- */

.sf-footer {
    position: relative;
    background: var(--sf-footer-bg);
    color: var(--sf-footer-text);
    margin-top: 0;
    padding-block: var(--sf-space-10) var(--sf-space-5);
    overflow: hidden;
}

/* A single brass rule across the top of the footer, and a very soft brand
   wash behind it. This is the last "once per viewport" accent on the page. */
.sf-footer::before {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: 0;
    height: 2px;
    background: linear-gradient(90deg, var(--sf-brand), transparent 55%);
}

.sf-footer a { color: var(--sf-footer-text); }
.sf-footer a:hover { color: var(--sf-footer-ink); }

/* Buttons keep their own ink: the link rule above painted the mast CTA's
   label with the footer link colour, which on an amber brand read grey on
   amber, near-invisible (verified live on tactical). */
.sf-footer a.sf-btn-primary,
.sf-footer a.sf-btn-primary:hover { color: var(--sf-on-brand); }

.sf-footer-grid {
    display: grid;
    gap: var(--sf-space-8);
    grid-template-columns: 1fr;
    padding-bottom: var(--sf-space-8);
}

/* Deliberately asymmetric: the NAP block gets nearly half the width and the
   store name is set at display size, so the footer reads as a sign-off
   rather than four equal columns of links. */
@media (min-width: 700px) {
    .sf-footer-grid { grid-template-columns: 1.9fr 1fr 1fr 1.3fr; gap: var(--sf-space-7); }
}

@media (min-width: 1100px) {
    .sf-footer-grid { grid-template-columns: 2.4fr 1fr 1fr 1.4fr; gap: var(--sf-space-9); }
}

.sf-footer-name {
    font-family: var(--sf-font-heading);
    font-weight: var(--sf-display-weight);
    text-transform: var(--sf-heading-transform);
    letter-spacing: calc(var(--sf-track-700) + var(--sf-track-offset));
    color: var(--sf-footer-ink);
    font-size: var(--sf-fs-700);
    line-height: var(--sf-lh-900);
    margin-bottom: var(--sf-space-4);
    max-width: 14ch;
}

/* :not(.sf-footer-name): this selector outranks .sf-footer-name (0,1,1 vs
   0,1,0) and was shrinking the display-size store name to body size on every
   theme (verified live on tactical). */
.sf-footer-nap p:not(.sf-footer-name) { color: var(--sf-footer-text); margin-bottom: 6px; font-size: var(--sf-fs-300); }

.sf-footer-heading {
    font-family: var(--sf-font-heading);
    color: var(--sf-footer-ink);
    font-weight: 600;
    font-size: var(--sf-fs-200);
    text-transform: uppercase;
    letter-spacing: var(--sf-track-caps);
    margin-bottom: var(--sf-space-4);
}

.sf-footer-col { display: flex; flex-direction: column; gap: 10px; }
.sf-footer-col a { font-size: var(--sf-fs-300); }

.sf-footer-hours { display: flex; flex-direction: column; gap: 5px; font-size: var(--sf-fs-300); }
.sf-footer-hours div { display: flex; justify-content: space-between; gap: 12px; }
/* nowrap: with a long range in the same row, the flexed dt otherwise breaks
   "Mon - Wed" onto two lines while its neighbours stay on one (verified
   live on heritage and tactical). */
.sf-footer-hours dt { color: var(--sf-footer-ink); font-weight: 600; white-space: nowrap; }
.sf-footer-hours dd { margin: 0; font-variant-numeric: tabular-nums; text-align: right; }

.sf-footer-legal {
    position: relative;
    padding-top: var(--sf-space-5);
    font-size: var(--sf-fs-200);
}

.sf-footer-legal::before {
    content: "";
    position: absolute;
    /* The gutter, not the container edge: at 0 the hairline started 56px
       left of the legal text it underlines (verified live). */
    inset-inline: var(--sf-gutter);
    top: 0;
    height: 1px;
    background: linear-gradient(90deg, var(--sf-footer-text), transparent 70%);
    opacity: 0.28;
}

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

.sf-footer-compliance {
    margin-bottom: var(--sf-space-2);
    /* --sf-measure resolves against this rule's small mono size to ~296px,
       which wrapped one sentence onto four cramped lines (verified live). */
    max-width: 80ch;
}

/* ---------- header commerce links (moved from 05-commerce) ---------- */

/* These render in the SHELL on every page of a full-commerce site, so they
   cannot live in a stylesheet that only ships on the commerce routes. */

.sf-header-commerce {
    display: flex;
    align-items: center;
    gap: var(--sf-space-4);
    margin-left: var(--sf-space-3);
    white-space: nowrap;
}

.sf-header-commerce a {
    text-decoration: none;
    font-family: var(--sf-font-heading);
    font-weight: 600;
    font-size: var(--sf-fs-300);
    letter-spacing: 0.02em;
    color: var(--sf-header-ink);
}

.sf-header-commerce a:hover { color: var(--sf-brand); text-decoration: none; }

.sf-cart-count {
    display: inline-block;
    min-width: 20px;
    margin-left: 5px;
    padding: 1px 6px;
    border-radius: var(--sf-radius-pill);
    background: var(--sf-brand);
    color: var(--sf-on-brand);
    font-size: var(--sf-fs-100);
    font-weight: 700;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* The empty badge pill: .sf-cart-count's display beat the hidden attribute,
   leaving a stray 20px blob beside "Cart" on an empty cart (UX-minor-12 r2). */
.sf-cart-count[hidden] { display: none; }

/* ================================================================
   ultraplan round 4: the shell as a designed surface.
   Appended, never inserted (cascade order).
   ================================================================ */

/* ---------- header condense-on-stick (motion spec 5.1) ----------

   CSS-only: the sticky header asks its own scroll-state container whether it
   is stuck to the top and condenses 16px, logo easing down with it. One-shot
   transitions, not scroll-linked animation. Engines without scroll-state
   queries (everything but Chromium 133+ today) skip the whole block and keep
   the full-height header. The BLUR IS NEVER TOUCHED in the stuck state:
   changing backdrop-filter radius re-renders the backdrop every frame. */
@media (prefers-reduced-motion: no-preference) {
    .sf-header-inner {
        transition: min-height var(--sf-dur-3) var(--sf-ease-out);
    }

    .sf-logo {
        transition: transform var(--sf-dur-3) var(--sf-ease-out);
    }
}

@container sf-header scroll-state(stuck: top) {
    .sf-header-inner {
        min-height: calc(var(--sf-header-height) - 16px);
    }

    .sf-logo {
        transform: scale(0.88);
        transform-origin: left center;
    }
}

/* ---------- the drawer menu is an index, not a dropdown (motion 5.2) ----------

   Menu typography goes BIG: display-face links at the fs-700 step, hairline
   separated, with mono ordinals - the index pattern. Scoped to the drawer
   copy of the nav only; the inline desktop nav keeps its quiet size. The
   phone link keeps its own compact treatment. */
.sf-nav-drawer > .sf-nav {
    min-width: min(86vw, 340px);
    padding: var(--sf-space-3) var(--sf-space-4) var(--sf-space-4);
    counter-reset: sf-menu;
}

.sf-nav-drawer > .sf-nav > a:not(.sf-nav-phone) {
    display: flex;
    align-items: baseline;
    gap: var(--sf-space-3);
    padding: var(--sf-space-3) var(--sf-space-2);
    border-radius: 0;
    font-size: var(--sf-fs-700);
    line-height: var(--sf-lh-700);
    letter-spacing: calc(var(--sf-track-700) + var(--sf-track-offset));
    font-weight: var(--sf-heading-weight);
    text-transform: var(--sf-heading-transform);
    box-shadow: inset 0 -1px 0 var(--sf-hairline);
}

.sf-nav-drawer > .sf-nav > a:not(.sf-nav-phone)::before {
    counter-increment: sf-menu;
    content: counter(sf-menu, decimal-leading-zero);
    font-family: var(--sf-font-mono);
    font-variant-numeric: tabular-nums slashed-zero;
    font-size: var(--sf-fs-100);
    letter-spacing: 0.02em;
    color: var(--sf-text-faint);
}

/* ---------- footer as sign-off (creative spec 1.9) ----------

   The wordmark zone: the store name as a word-graphic at the fs-1100 step,
   full width and CLIPPED at the right edge (overflow: clip - never hidden,
   which would trap sticky descendants; never a horizontal scrollbar), in
   low-opacity footer ink. The primary CTA and the phone sit ON its lower
   third via a one-cell grid stack. This is the absorbed CTA band: the page's
   closing accent, on every tenant page. */
.sf-footer-mast {
    display: grid;
    /* The nowrap wordmark's max-content would otherwise size the single
       track (700px+ on a phone), so the actions cluster never met a
       constraint, never wrapped, and the phone number was clipped mid-glyph
       at 390px (verified live). Pinning the track to the mast's own width
       lets the wordmark overflow into the clip as designed while the CTA and
       phone wrap like a normal flex row. */
    grid-template-columns: minmax(0, 100%);
    overflow: clip;
    padding-block-start: var(--sf-pad-chapter);
}

.sf-footer-mast > * {
    grid-area: 1 / 1;
}

.sf-footer-wordmark {
    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;
    color: var(--sf-footer-ink);
    opacity: 0.12;
    /* The page's content edge, not the viewport gutter: a bare gutter put
       the wordmark (and the CTA cluster below, same fix) 72px left of every
       other footer column on desktop - three different left edges in one
       footer (verified live). The max() collapses back to the plain gutter
       on viewports narrower than the container. */
    padding-inline-start: max(var(--sf-gutter), calc((100% - var(--sf-container)) / 2 + var(--sf-gutter)));
    user-select: none;
    pointer-events: none;
}

.sf-footer-mast-actions {
    align-self: end;
    justify-self: start;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sf-space-4) var(--sf-space-7);
    /* Same content-edge alignment as the wordmark above. */
    padding-inline: max(var(--sf-gutter), calc((100% - var(--sf-container)) / 2 + var(--sf-gutter))) var(--sf-gutter);
    /* Real clearance from the sitemap grid below; space-2 left the cluster
       floating in the wordmark's descender zone (verified live). */
    padding-block-end: var(--sf-space-6);
}

/* The absorbed CTA earns the large cut: it is the page's closing action. */
.sf-footer-mast-cta {
    font-size: var(--sf-fs-400);
    padding: var(--sf-space-3) var(--sf-space-7);
    /* Contrast ring: on themes whose brand is close to the footer ground
       (blackout: black button on black footer, verified live) the button
       otherwise disappears into the mast entirely. A faint footer-ink ring
       keeps the button legible as a button on every ground and is invisible
       noise on themes where the brand already contrasts. */
    box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--sf-footer-ink) 40%, transparent);
}

.sf-footer-mast-phone {
    font-family: var(--sf-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: var(--sf-fs-600);
    letter-spacing: 0.02em;
    color: var(--sf-footer-ink);
    /* One object, two voices: the phone shares the button's optical line. */
    line-height: 1;
    box-shadow: inset 0 -2px 0 var(--sf-rule-accent, var(--sf-brand));
    padding-bottom: 6px;
    transition: color var(--sf-dur-2) var(--sf-ease-out);
}

.sf-footer-mast-phone:hover,
.sf-footer-mast-phone:focus-visible {
    color: var(--sf-brand-strong, var(--sf-brand));
}

/* No dark logo variant uploaded: the primary mark on a dark ground becomes a
   light silhouette instead of mud. brightness(0) flattens every pixel to
   black, invert(1) lifts the silhouette to white, and the slight opacity
   settles it into the footer ink. The flag (and this filter) disappear the
   moment a real LogoDark exists - the designed mark always beats the derived
   one. Applies to any raster; SVGs never reach the logo pipeline. */
.sf-logo-forced-light {
    filter: brightness(0) invert(1) opacity(0.92);
}

/* Sitemap columns: mono caps headers sitting over hairlines (boxes are the
   last resort; a hairline and a lightness step do the separating). */
.sf-footer-heading {
    font-family: var(--sf-font-mono);
    font-size: var(--sf-fs-100);
    letter-spacing: var(--sf-track-caps);
    padding-bottom: var(--sf-space-2);
    box-shadow: inset 0 -1px 0 var(--sf-hairline);
}

/* The open-now status line under the hours table: server-computed from the
   same OpenNowCalculator the hours block uses, same output-cache staleness
   budget as that block. Mono, because it is metadata (law 5). */
.sf-footer-open {
    font-family: var(--sf-font-mono);
    font-size: var(--sf-fs-200);
    letter-spacing: 0.02em;
    color: var(--sf-footer-ink);
    margin-top: var(--sf-space-3);
}

/* Compliance small print: fs-100 mono over the final hairline. FFL boilerplate
   reads as a record, not as copy, and mono is how records are set here. */
.sf-footer-legal {
    font-family: var(--sf-font-mono);
    font-size: var(--sf-fs-100);
    letter-spacing: 0.02em;
}

.sf-footer-col a:hover {
    text-decoration: none;
}

/* ---------- view-transition chrome stability (motion spec 3.1) ----------

   The sticky chrome must not cross-fade with the page: naming the header and
   announcement gives them their own transition groups, and disarming those
   groups pins the chrome dead-still while the content fades under it. The
   names are scoped to the desktop query that enables @view-transition in
   01-foundation. The root fade tightens to dur-2: a 200ms cross-fade reads
   as response, anything longer as decoration. */
@media (min-width: 900px) {
    .sf-header { view-transition-name: sf-header; }
    .sf-announcement { view-transition-name: sf-announcement; }
}

::view-transition-group(sf-header),
::view-transition-group(sf-announcement) {
    animation: none;
}

::view-transition-old(root),
::view-transition-new(root) {
    animation-duration: var(--sf-dur-2);
}

/* ---------- cart badge bump (motion spec 2.5) ----------

   The badge carries a view-transition name so a same-origin navigation never
   cross-fades it against unrelated chrome, and cart-count.js toggles the
   bump class only when the fetched count DIFFERS from what the page shows.
   Transform-only spring; if linear() is unsupported the animation is simply
   dropped. */
.sf-cart-count {
    view-transition-name: sf-cart-count;
}

@media (prefers-reduced-motion: no-preference) {
    .sf-cart-count.sf-bump {
        animation: sf-badge-pop var(--sf-dur-3) var(--sf-ease-spring, var(--sf-ease-out));
    }
}

@keyframes sf-badge-pop {
    from { transform: scale(0.6); }
    to { transform: scale(1); }
}

/* ---------- narrow-viewport header fit (theme QA r5) ----------

   The wide-set heading faces (heritage, walnut-club, classic-brick, arctic,
   big-woods) overflow a 390px viewport by 9-14px at the fs-600 wordmark:
   Cart clips off the right edge and the page scrolls sideways. One step down
   on the wordmark plus tightened cluster gaps clears the worst face with
   ~40px to spare; min-width: 0 + overflow lets an unusually long store name
   shrink-and-clip instead of pushing the shell wide. Drawer-only widths, so
   the desktop masthead never changes. */
@media (max-width: 519px) {
    .sf-header-inner { gap: var(--sf-space-3); }

    .sf-logo {
        font-size: var(--sf-fs-500);
        min-width: 0;
        overflow: hidden;
    }

    .sf-header-commerce {
        gap: var(--sf-space-3);
        margin-left: 0;
    }
}
