/* ============================================================================
   Nouri Narie 2.0 — components

   The shared vocabulary for all 15 pages. Loads after base.css.

   Where a component draws from one reference more than the other it says so,
   because the fusion only holds if each piece is deliberately assigned rather
   than blended. Kylie contributes density: the ticker, the marquee, the
   stickers, the buy box, the stepper, the assurance strip. Rhode contributes
   restraint: the accordion, the media bands, the type in every one of them.
   ========================================================================= */

/* ── ticker ────────────────────────────────────────────────────────────────
   Kylie. The announcement bar every cosmetics storefront runs, at 32px so it
   costs a third of a nav. The duplicate track is aria-hidden — a screen reader
   should hear the promise once, not twice. */

.ticker {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: var(--z-ticker);
  height: var(--h-ticker);
  display: flex;
  align-items: center;
  overflow: hidden;
  background: var(--graphite);
  color: var(--on-dark);
}

.ticker-track {
  display: flex;
  flex: none;
  gap: var(--s-10);
  padding-inline: var(--s-5);
  white-space: nowrap;
}

.ticker-item {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  /* --on-dark-mute, not --on-dark. The announcement was the LOUDEST thing on
     every page: full-bleed uppercase mono at 11.50:1, sitting directly above a
     navigation whose links were --ink-muted at 5.50:1. The hierarchy was
     inverted — an assurance strip out-shouting the way around the store, on
     every page, permanently. At 6.80:1 the band is still comfortably past AA for
     its size and still perfectly readable; it just stops being the first thing
     the eye lands on. The nav links below take the strength it gives up. */
  color: var(--on-dark-mute);
}

/* Without JS or under reduced motion the track never animates, so a second
   copy would just read as a stutter. site.js only adds .is-looping once it has
   actually started the tween. */
.ticker:not(.is-looping) .ticker-track + .ticker-track { display: none; }

/* The fade lives on the OVERFLOW CONTAINER, not on the track.

   It used to sit on .ticker-track / .marquee-track, and a mask on the track
   travels WITH the track — a soft patch sliding across the screen rather than a
   fixed edge treatment. That is the only reason the old rule had to be scoped
   `:not(.is-looping)`, and it is why the animated state (the default, and the
   state in every screenshot) hard-chopped BOTH edges: desktop opened mid-word on
   "THIN CANADA ONLY", mobile on "HIN CANADA ONLY". On the container the mask is
   viewport-anchored, so it is correct while moving and while still, and the
   scoping is no longer needed.

   The two states DO want different masks, and this is where I depart from the
   audit's proposed fix. It suggested one symmetric `transparent → 40px` ramp for
   both states. That is right for the moving case and wrong for the static one:
   with no animation the track is left-aligned and its first character sits at
   the 20px `padding-inline`, so a symmetric mask fades the opening word of
   "STANDARD SHIPPING $6 CAD" — under `prefers-reduced-motion` it would replace
   one unreadable edge with another. So the left ramp is applied only under
   .is-looping, where content genuinely does run off that edge.

   The right ramp is unconditional and costs nothing when nothing overflows: the
   static track measures 1421px against a 1440px viewport, and the last item ends
   ~1401px, so at desktop the 40px it dims is empty container. Below 1421px the
   track really does overflow and the fade reads as "there is more" rather than
   as markup that failed. 40px is small enough that a fully-visible item can
   never be lost — worst case it dims 40px of a 216px item already at the edge.

   This also un-breaks the accessibility path outright. The old mask ran
   `#000 82% → transparent 99%` across a 1421px track, so 82% landed at 1165px —
   the start of the fourth item — and a reduced-motion visitor at 1440px read
   "SECURE" solid, "CHECKOUT BY" grey and "STRIPE" gone. The payment-trust line
   was being deleted for precisely the cohort least able to spare it. */
.ticker,
.marquee {
  /* 40px is enough at desktop, where the track barely overflows. On a phone the
     same 40px is ~10% of the viewport and a truncated word still lands hard
     enough to read as broken markup: at 390 the bar opens on "ITHIN CANA".
     Widening the ramp to a share of the viewport makes the cut read as motion
     rather than as a clipping bug. Capped so it can never eat a whole item. */
  --fade: clamp(40px, 14vw, 72px);
  -webkit-mask-image: linear-gradient(to right, #000 0,
                                      #000 calc(100% - var(--fade)), transparent 100%);
          mask-image: linear-gradient(to right, #000 0,
                                      #000 calc(100% - var(--fade)), transparent 100%);
}
.ticker.is-looping,
.marquee.is-looping {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade),
                                      #000 calc(100% - var(--fade)), transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 var(--fade),
                                      #000 calc(100% - var(--fade)), transparent 100%);
}

/* ── nav ───────────────────────────────────────────────────────────────────
   Both references. Kylie supplies the cart count and the account affordance,
   Rhode supplies the refusal to build a mega-menu for five links.

   Opaque, never glass. v1 blurred this; both reference systems name
   backdrop-filter an anti-pattern and neither reference site uses it. */

.nav {
  position: fixed;
  inset: var(--h-ticker) 0 auto 0;
  z-index: var(--z-nav);
  height: var(--h-nav);
  display: flex;
  align-items: center;
  background: var(--paper);
  transition: box-shadow var(--d-base) var(--ease),
              border-color var(--d-base) var(--ease);
  /* Not transparent at rest. The bar is opaque --paper and the hero beneath it
     opens near-white on the left, so an invisible edge measured 1.01:1 against
     the page below and the nav row dissolved across the left third of a 1440
     viewport. --rule-hair is the faintest step that still reads as an edge;
     .is-stuck escalates to --rule-soft plus the shadow as the scroll cue, so
     the two states stay distinguishable. */
  border-bottom: 1px solid var(--rule-hair);
}

.nav.is-stuck {
  border-bottom-color: var(--rule-soft);
  box-shadow: var(--shadow-1);
}

/* Three zones: brand | links | actions. This is a GRID and not the flex row it
   was, because the two are not the same thing here. The old rule centred the
   link list by putting `margin-right: auto` on the logo, which does not centre
   anything — it pushes. All seven remaining affordances ended up in the right
   46% of the bar with 270px of dead space between the wordmark and "Shop" at
   1440, and 128px at 1024. `1fr auto 1fr` centres the middle zone against the
   MEASURE regardless of how wide the two side zones are, so the link list stays
   optically centred whether the cart badge is showing a 1 or a 12.

   Below 1024 the middle column collapses (the links leave the bar for the fixed
   panel) and the layout falls back to `auto 1fr`: brand hard left, actions hard
   right, nothing floating in between. */
.nav-inner {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--s-6);
}

.nav-logo {
  font-family: var(--font-display);
  font-variation-settings: 'wght' 800, 'wdth' 88;
  font-size: 1.0625rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  /* inline-flex so the two brackets sit on the text's own optical centre line
     instead of the baseline. justify-self keeps the <a> hit area to the width of
     the name rather than stretching it across the whole 1fr column — a grid item
     stretches by default, and an invisible 400px-wide home link swallowing the
     left third of the bar is a real mis-click, not a cosmetic one. */
  display: inline-flex;
  align-items: center;
  justify-self: start;
}

/* The [ ] mark. It is now TWO svgs flanking the name — see shell.BRACKET_OPEN.
   overflow: visible because non-scaling-stroke draws the outer half of the 1px
   stroke outside the declared box.

   The margin is --s-2 on the inside edge of each bracket only. The old comment
   held this at --s-2 and refused to spend more, citing nav()'s note that four
   affordances measured 385px against a 375px viewport — correct for the bar that
   note describes, but that is not the bar that ships. Measured at 390px before
   this change: the logo ended at x=166 and Cart began at x=264, which is 98px of
   slack; at 360px it is 68px. A second bracket plus one --s-2 gap costs ~13px
   against that. The budget does not block it, and the closing bracket is what
   makes the mark read as a bracket at all rather than as two specks. */
.nav-logo .bracket {
  flex: none;
  overflow: visible;
  color: var(--alu-shadow);
  transition: color var(--d-fast) var(--ease);
}
.nav-logo .bracket:first-child { margin-right: var(--s-2); }
.nav-logo .bracket:last-child  { margin-left: var(--s-2); }
/* The brackets are the hover state. The name itself does not move or change
   colour — it is the brand, not a control — but the mark closing in confirms the
   whole thing is one target. */
.nav-logo:hover .bracket { color: var(--ink); }

.nav-primary { justify-self: center; }

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav-links a {
  position: relative;
  display: inline-block;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-xs);
  font-size: var(--t-sm);
  /* --ink at 13.21:1, not --ink-muted at 5.50:1. The navigation is the primary
     way around the store and it was set two steps quieter than the announcement
     bar above it. See the note on .ticker-item: the strength the ticker gave up
     is spent here. */
  color: var(--ink);
  text-decoration: none;
  transition: color var(--d-fast) var(--ease), background var(--d-fast) var(--ease);
}
.nav-links a:hover { background: var(--sage-wash); }

/* The current page is marked by a RULE, not by a colour. Colour alone was doing
   the work before (--ink-muted links, --ink current), which is a 1.4.1 failure
   on its own and was in any case invisible once the links went to --ink above.
   The underline is drawn as a sibling-independent ::after rather than
   text-decoration so it sits clear of the descenders and lines up across items
   of different lengths. */
.nav-links a[aria-current='page']::after {
  content: '';
  position: absolute;
  left: var(--s-3);
  right: var(--s-3);
  bottom: 2px;
  height: 1.5px;
  background: var(--sage-deep);
  border-radius: var(--r-pill);
}

.nav-actions { display: flex; align-items: center; justify-self: end; gap: var(--s-2); }

.nav-cart {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  background: transparent;
  font-family: var(--font-text);
  font-size: var(--t-sm);
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--d-fast) var(--ease), color var(--d-fast) var(--ease),
              border-color var(--d-fast) var(--ease);
}
.nav-cart:hover { background: var(--ink); color: var(--on-dark); border-color: var(--ink); }

/* Kylie's count badge. Reads "0" as a muted dot rather than hiding, so the
   control never changes width when the first item lands.

   That was the stated intent and it was NOT what shipped. js/cart.js
   paintBadge() — copied verbatim from live, uneditable — writes
   `el.textContent = String(n)` and then toggles the `hidden` attribute. Two
   consequences: the element always carries a text node so it is never :empty,
   which made the old `.nav-count:empty::after { content: '0' }` dead code; and
   `[hidden]` won, so the badge measured 0x0 at rest and the Cart pill jumped
   wider the moment the first item landed — the exact regression the comment
   claimed to have designed out.

   The audit's proposed fix was `display: inline-block` on .nav-count to outrank
   [hidden]. That does not work here: css/base.css:14 is
   `[hidden] { display: none !important }`, and its comment explains why it has
   to be — cart.js, account.js and admin.js all switch panels by toggling
   [hidden], so an author display rule that beats it breaks three files. Winning
   that specificity fight would be fixing a badge by breaking the account page.

   Instead the zero is its OWN element that cart.js never sees (shell.nav()), and
   the two share one grid cell. The slot reserves the badge's width exactly once,
   so it is identical in both states, and the swap is driven off the `hidden`
   attribute cart.js is already maintaining — no extra class, nothing to keep in
   sync with a file that cannot be edited. */
.nav-count-slot {
  display: grid;
  grid-template-areas: 'count';
  flex: none;
}
.nav-count,
.nav-count-zero {
  grid-area: count;
  min-width: 1.375rem;
  padding-inline: var(--s-1);
  border-radius: var(--r-pill);
  /* --font-text, not --font-mono. DM Mono draws a slashed zero, so an empty
     cart rendered as a crossed-out glyph that reads as "null" or "no cart"
     rather than "nothing in it" — the one state this badge exists to show.
     tabular-nums still holds the width steady across the swap. */
  font-family: var(--font-text);
  font-size: var(--t-micro);
  font-variant-numeric: tabular-nums;
  line-height: 1.375rem;
  text-align: center;
  transition: background var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}
/* Zero is styled DOWN, not hidden. An empty badge painted in solid --sage reads
   as a notification, and a permanent "you have 0 things" alarm is noise.
   --ink-muted on --rule-hair measures 4.97:1. */
.nav-count-zero { background: var(--rule-hair); color: var(--ink-muted); }
/* --shell on --sage measures 4.58:1. */
.nav-count { background: var(--sage); color: var(--shell); }

/* Exactly one of the two is visible, and `hidden` on the live badge IS the
   empty state — cart.js only removes it when n > 0. `:has()` lets the zero react
   to its sibling's attribute without a second write from JS. The default is
   VISIBLE: before cart.js has run the badge carries neither content nor
   [hidden], and showing the zero is the correct first paint. */
.nav-count-slot:has(.nav-count:not([hidden])) .nav-count-zero { visibility: hidden; }

/* The suffix is only meaningful when a real count precedes it, and the empty
   phrase only when it does not. Exactly one is in the accessibility tree. */
.nav-count-slot:has(.nav-count[hidden]) .nav-count-suffix { display: none; }
.nav-count-slot:has(.nav-count:not([hidden])) .nav-count-empty { display: none; }

/* On the hovered pill the resting zero would sit --rule-hair on --ink, a light
   blob on a dark ground that reads as the filled state. */
.nav-cart:hover .nav-count-zero { background: var(--on-dark-mute); color: var(--graphite); }

.nav-burger {
  display: none;
  position: relative;
  width: 40px; height: 40px;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--rule);
  border-radius: var(--r-xs);
  background: transparent;
  cursor: pointer;
  transition: background var(--d-fast) var(--ease), border-color var(--d-fast) var(--ease);
}
/* Three bars drawn as one element plus two box-shadows. The shadows cannot be
   animated into an X independently of each other, so the open state re-draws the
   pair as real ::before/::after bars and drops the shadows. Both sets occupy the
   same 16x1.5 box, so nothing moves when the swap happens. */
.nav-burger span {
  display: block;
  width: 16px; height: 1.5px;
  background: var(--ink);
  box-shadow: 0 -5px 0 var(--ink), 0 5px 0 var(--ink);
  transition: box-shadow var(--d-fast) var(--ease), background var(--d-fast) var(--ease);
}
.nav-burger span::before,
.nav-burger span::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 16px; height: 1.5px;
  margin-left: -8px;
  background: var(--ink);
  opacity: 0;
  transform: rotate(0deg);
  transition: transform var(--d-base) var(--ease), opacity var(--d-fast) var(--ease);
}
/* The burger becomes a close mark. aria-expanded already carries the state for
   assistive tech; this is the same information for everyone else, and it is the
   panel's only visible dismissal besides Escape. */
.nav.is-open .nav-burger span { background: transparent; box-shadow: none; }
.nav.is-open .nav-burger span::before { opacity: 1; transform: rotate(45deg); }
.nav.is-open .nav-burger span::after  { opacity: 1; transform: rotate(-45deg); }
.nav.is-open .nav-burger { border-color: var(--ink); background: var(--sage-wash); }

/* One of these two is always hidden — see shell.nav(). Above 1024 the account
   is a quiet button in the bar; below it, the bar has no room and the link
   lives in the burger panel instead. */
.nav-account { display: none; }

@media (max-width: 1023px) {
  .nav-account { display: block; }
  .nav-actions .btn--quiet { display: none; }

  /* The middle column is empty below 1024 — .nav-links leaves the bar for the
     fixed panel — so the bar becomes two zones rather than three with a hole in
     the middle. The measured balance at 390 before this: logo 16→166, then 98px
     of nothing, then Cart 264→326 and burger 334→374. The observed "cramped"
     was actually LOPSIDED — a third of the bar wordmark, a quarter empty. */
  .nav-inner { grid-template-columns: auto 1fr; gap: var(--s-3); }
  .nav-primary { display: contents; }

  .nav-burger { display: inline-flex; }
  .nav-links {
    position: fixed;
    /* A SHEET, not a strip. `bottom: auto` made this a top-anchored band whose
       height was whatever six links happened to measure, so the dark footer
       showed below it and the page scrolled freely behind it — the panel stayed
       pinned at y=88 while the document travelled, parking a light panel over
       the footer with "Contact Us" and "Your Cart" sliced through at its lower
       edge. Anchoring the bottom to the viewport makes it a sheet that covers
       what is behind it, which is what it always claimed to be. */
    inset: calc(var(--h-ticker) + var(--h-nav)) 0 0 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--s-3) var(--gutter) var(--s-6);
    background: var(--paper);
    border-bottom: 1px solid var(--rule-hair);
    box-shadow: var(--shadow-2);
    /* The sheet can now be taller than its content and shorter than its links on
       a small landscape phone, so it owns its own scrolling. overscroll-behavior
       stops a flick at the end of the list chaining through to the document
       underneath. */
    overflow-y: auto;
    overscroll-behavior: contain;
    transform: translateY(-8px);
    opacity: 0;
    pointer-events: none;
    /* visibility is load-bearing, not belt-and-braces. opacity and
       pointer-events hide the panel from eyes and mouse but leave all six
       links in the tab order and the accessibility tree, so a phone visitor
       tabbing from the logo falls into a menu they cannot see. v1 carried this
       exact rule with a comment saying so; v2 dropped it and reintroduced the
       bug. It is not animatable, so it is stepped at the end of the
       transition rather than eased. */
    visibility: hidden;
    transition: opacity var(--d-base) var(--ease),
                transform var(--d-base) var(--ease),
                visibility 0s linear var(--d-base);
  }
  .nav.is-open .nav-links {
    opacity: 1;
    transform: none;
    pointer-events: auto;
    visibility: visible;
    transition-delay: 0s;
  }
  /* Locking the document while the sheet is open. js/site.js only toggles
     .is-open and cannot be edited from here, so the lock is derived from that
     one class with :has() rather than needing a second write. Widely supported
     since 2023; if it ever fails the panel simply behaves as it did before,
     which is a degradation and not a break.

     scrollbar-gutter is what stops the page shifting sideways by the scrollbar
     width at the moment the lock lands — on a phone that is 0 and this does
     nothing, but the same rule runs on a small laptop window. */
  html:has(.nav.is-open) { overflow: hidden; scrollbar-gutter: stable; }

  .nav-links a {
    /* block, not the desktop inline-block. An inline-block anchor is only as
       wide as its text, so the row divider below stopped at the end of the word
       — six rules of six different lengths down the left of the sheet, which
       reads as ragged rather than as a list. It also meant the tap target for
       "Shop" was 64px wide in a 390px sheet. */
    display: block;
    padding: var(--s-4) var(--s-3);
    font-size: var(--t-lede);
    border-radius: 0;
  }
  /* The divider goes on the <li> so it spans the sheet regardless of the
     anchor's box. The links are the only content of a full-height sheet now, so
     they carry their own separation — without a rule the list reads as one
     undifferentiated column of text in a large empty panel. */
  .nav-links li + li { border-top: 1px solid var(--rule-hair); }
  /* The bottom-anchored ::after from the desktop current-page rule would collide
     with the row divider. On a sheet the marker moves to the leading edge. */
  .nav-links a[aria-current='page']::after {
    left: calc(var(--s-3) * -1);
    right: auto;
    top: var(--s-3);
    bottom: var(--s-3);
    width: 2px;
    height: auto;
  }
}

/* Every page pushes its own content clear of the fixed chrome. The film is the
   one exception — it owns the viewport and insets would break the scrub
   engine's measurement. */
.page-top { padding-top: var(--h-chrome); }

/* ── marquee ───────────────────────────────────────────────────────────────
   Kylie. Louder and larger than the ticker, and it sits in the page rather
   than over it. */

/* display:flex is load-bearing, exactly as it is on .ticker above. The band
   ships two identical tracks and site.js slides both by xPercent:-100 so the
   second becomes the first's tail. Without a flex container the tracks are
   block boxes and stack VERTICALLY: the same sentence renders on two lines, the
   band is twice as tall as it should be, and because both tracks leave to the
   left together the loop empties the band instead of refilling it. The
   `flex: none` on .marquee-track below only means anything in this context. */
.marquee {
  display: flex;
  align-items: center;
  overflow: hidden;
  padding-block: var(--s-5);
  border-block: 1px solid var(--rule);
  background: var(--alu-light);
}
.marquee-track {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--s-8);
  white-space: nowrap;
}
.marquee-item {
  font-family: var(--font-display);
  font-variation-settings: 'wght' 800, 'wdth' 88;
  font-size: clamp(1.25rem, 3vw, 2rem);
  letter-spacing: var(--ls-head);
  text-transform: uppercase;
}
.marquee-sep { color: var(--sage); flex: none; }
.marquee:not(.is-looping) .marquee-track + .marquee-track { display: none; }

/* ── card ──────────────────────────────────────────────────────────────────
   Kylie's density, Rhode's restraint. The 1px border is the load-bearing
   decision: both reference sites draw a genuinely visible rule on a card
   rather than floating it on a soft shadow, and that single choice is what
   keeps a three-up product grid from looking like every other generated
   storefront. No shadow at rest. */

.card {
  display: flex;
  flex-direction: column;
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  overflow: hidden;
  transition: transform var(--d-base) var(--ease), box-shadow var(--d-base) var(--ease);
}
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-2); }

/* The image is taken OUT OF FLOW, and that is the whole point of this rule.

   With the <img> in normal flow, `height: 100%` resolves against a parent
   whose own height is still being derived from aspect-ratio — circular, so the
   browser falls back to the image's intrinsic shape, and that content then
   stretches the container past the ratio it just declared. aspect-ratio only
   holds when nothing else is sizing the box.

   The three product shots are 900x900, 900x1350 and 900x900, so the toner card
   rendered 343x515 against its siblings' 343x428: one tile in a three-up grid
   87px taller than the other two, with its title and price knocked out of
   alignment. Absolute positioning removes the image as a size input and the
   declared 4:5 becomes real. */
.card-media {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--mist);
  border-bottom: 1px solid var(--rule-hair);
}
.card-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.card-body { display: flex; flex-direction: column; gap: var(--s-2); padding: var(--s-5); }
.card-title { font-size: var(--t-h4); }
.card-title a { text-decoration: none; }
.card-note { font-size: var(--t-sm); color: var(--ink-muted); }

.card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  margin-top: auto;
  padding: var(--s-4) var(--s-5) var(--s-5);
}
.card-price { font-family: var(--font-mono); font-size: var(--t-sm); }

/* ── sticker ───────────────────────────────────────────────────────────────
   Kylie. The badge language a cosmetics grid runs on. --ember is capped at one
   per viewport by convention — an alarm colour used three times is wallpaper. */

.sticker {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-1) var(--s-2);
  border-radius: var(--r-xs);
  background: var(--alu-light);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  white-space: nowrap;
}
.sticker--step  { background: var(--ink); color: var(--on-dark); }
.sticker--alert { background: var(--ember); color: var(--shell); }
.sticker--quiet { background: transparent; border: 1px solid var(--rule-soft); color: var(--ink-muted); }
.sticker--pill  { border-radius: var(--r-pill); padding-inline: var(--s-3); }

.card-media .sticker { position: absolute; top: var(--s-3); left: var(--s-3); }

/* ── button ────────────────────────────────────────────────────────────────
   Both. Rhode's 7px, Kylie's weight. The primary carries --shadow-3 on hover —
   Rhode's one rose-tinted float, and the only place most pages spend it. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-6);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  font-family: var(--font-text);
  font-size: var(--t-sm);
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--d-fast) var(--ease),
              color var(--d-fast) var(--ease),
              border-color var(--d-fast) var(--ease),
              box-shadow var(--d-base) var(--ease);
}

.btn--primary { background: var(--ink); color: var(--on-dark); }
.btn--primary:hover:not([disabled]):not([aria-disabled='true']) {
  background: var(--sage-deep);
  box-shadow: var(--shadow-3);
}

.btn--ghost { background: transparent; border-color: var(--rule); color: var(--ink); }
.btn--ghost:hover:not([disabled]):not([aria-disabled='true']) { background: var(--ink); color: var(--on-dark); border-color: var(--ink); }

.btn--quiet { background: transparent; color: var(--ink-muted); padding-inline: var(--s-3); }
.btn--quiet:hover { color: var(--ink); }

.btn--block { width: 100%; }
.btn--lg { padding: var(--s-4) var(--s-8); font-size: var(--t-body); }

/* btn--sm lives at system level rather than on one page because three copied,
   uneditable scripts construct it: js/account.js (sign out, request a return),
   js/admin.js (every save, refresh and toggle in the console) and js/cart.js.
   Defined anywhere narrower, the next page to grow a small button gets an
   unstyled one and nobody finds out until it ships. */
.btn--sm { padding: var(--s-2) var(--s-4); font-size: var(--t-caption); }

/* js/cart.js line 295 builds the cross-sell CTA as `btn btn--pink btn--sm` and
   that file is copied verbatim from the live site — the class cannot be
   renamed here. The name is a leftover from v1's palette; the button it draws
   is the most commercially important control on the cart page, and with no
   rule it renders transparent-on-white and effectively disappears.

   Do not "clean this up". Deleting it does not produce a lint warning, it
   produces an invisible buy button. */
.btn--pink { background: var(--sage); color: var(--shell); }
.btn--pink:hover:not([disabled]) { background: var(--sage-deep); }

/* On a dark ground the ink button disappears. */
.band--graphite .btn--primary { background: var(--on-dark); color: var(--graphite); }
.band--graphite .btn--ghost   { border-color: var(--on-dark-mute); color: var(--on-dark); }
.band--graphite .btn--ghost:hover { background: var(--on-dark); color: var(--graphite); }

/* ── buy box ───────────────────────────────────────────────────────────────
   Kylie. Sticky from 1024 up, and the one element on a PDP that spends
   --shadow-3. Opaque blush, not glass. */

.buybox {
  background: var(--mist);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  padding: var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}
@media (min-width: 1024px) {
  .buybox {
    position: sticky;
    top: calc(var(--h-chrome) + var(--s-6));
    box-shadow: var(--shadow-3);
  }
}
.buybox-price { font-family: var(--font-mono); font-size: var(--t-lede); }
.buybox-meta  { display: flex; flex-wrap: wrap; gap: var(--s-2); }

/* ── stepper ───────────────────────────────────────────────────────────── */

.stepper {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  overflow: hidden;
  background: var(--shell);
}
.stepper button {
  width: 34px; height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  font-size: var(--t-body);
  cursor: pointer;
  transition: background var(--d-fast) var(--ease);
}
.stepper button:hover:not([disabled]) { background: var(--sage-wash); }
.stepper input {
  width: 40px;
  border: 0;
  background: transparent;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  color: inherit;
  -moz-appearance: textfield;
}
.stepper input::-webkit-outer-spin-button,
.stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* ── field ─────────────────────────────────────────────────────────────────
   Kylie's 4px — dense UI, not a card. */

.field { display: flex; flex-direction: column; gap: var(--s-2); }
.field label { font-size: var(--t-caption); font-weight: 600; }

.field input,
.field textarea,
.field select {
  width: 100%;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--rule);
  border-radius: var(--r-xs);
  background: var(--shell);
  font-family: var(--font-text);
  font-size: var(--t-body);
  color: var(--ink);
  transition: border-color var(--d-fast) var(--ease), box-shadow var(--d-fast) var(--ease);
}
.field input:focus,
.field textarea:focus,
.field select:focus {
  outline: none;
  border-color: var(--sage-deep);
  box-shadow: 0 0 0 3px var(--sage-wash);
}
.field textarea { min-height: 140px; resize: vertical; }
.field-error { font-size: var(--t-caption); color: var(--danger); }
.field-hint  { font-size: var(--t-caption); color: var(--ink-muted); }

/* ── accordion ─────────────────────────────────────────────────────────────
   Rhode. Native <details> so it works with JS off and keeps its semantics;
   GSAP only animates the height. */

.acc { border-top: 1px solid var(--rule-soft); }
.acc details { border-bottom: 1px solid var(--rule-soft); }

.acc summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-5) 0;
  font-size: var(--t-lede);
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.acc summary::-webkit-details-marker { display: none; }
.acc summary::after {
  content: '';
  flex: none;
  width: 10px; height: 10px;
  border-right: 1.5px solid var(--ink-muted);
  border-bottom: 1.5px solid var(--ink-muted);
  transform: rotate(45deg) translateY(-3px);
  transition: transform var(--d-base) var(--ease);
}
.acc details[open] summary::after { transform: rotate(-135deg) translateY(-3px); }
.acc-body { padding-bottom: var(--s-5); color: var(--ink-soft); max-width: 68ch; }

/* ── note ──────────────────────────────────────────────────────────────────
   Kylie's structure only.

   IMPORTANT: this is not a testimonial and must never be written as one. Nouri
   Narie has no customer reviews, and the band this lives in is framed as "how
   to read your own skin" — the brand's own attribution argument, attributed to
   the brand. A generated quote from a fictional customer would be a fabricated
   endorsement. */

.note {
  background: var(--mist);
  border: 1px solid var(--rule-hair);
  border-radius: var(--r-md);
  padding: var(--s-6);
}
.note p { font-size: var(--t-lede); line-height: var(--lh-body); }
.note cite {
  display: block;
  margin-top: var(--s-4);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  font-style: normal;
  color: var(--ink-muted);
}

/* ── accent emphasis ───────────────────────────────────────────────────────
   The copy deck carries `<em class="accent">…</em>` inside 20 headings — v1's
   way of stressing part of a title. The rule sweeps in from the left when the
   heading enters; site.js only adds .is-in, the motion is all CSS.

   The word stays --ink. Colouring it --sage would put a 4.0:1 word inside a
   13.7:1 sentence, which is both an AA failure and a worse-looking heading.

   Drawn as a background gradient rather than an absolutely-positioned ::after,
   and deliberately NOT `white-space: nowrap`. Ten of the nineteen accent
   phrases in the deck are several words — "never need each other", "this is
   the order", "clearly explained" — so the one-word assumption both of those
   techniques relied on is not true of the copy. Under nowrap, story.html's h1
   measured 425px against a 375px viewport and the document scrolled sideways
   by 78px. Note that body's `overflow-x: clip` does not save it: `clip` is the
   one overflow value that does not propagate from body to the viewport, so it
   clips nothing here.

   box-decoration-break keeps the rule under every line of a wrapped phrase
   rather than one stretched band under the last. background-size cannot be
   composited on the GPU, which is why the ::after existed — but this is a
   one-shot 350ms transition on at most a few elements per viewport, not a
   scrubbed one, and being readable at 375px is worth more than the layer. */

.accent {
  font-style: normal;
  background-image: linear-gradient(var(--sage), var(--sage));
  background-repeat: no-repeat;
  background-position: 0 calc(100% - 0.02em);
  background-size: 0 0.09em;
  transition: background-size var(--d-slow) var(--ease-out);
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
.accent.is-in,
html:not(.js) .accent { background-size: 100% 0.09em; }

@media (prefers-reduced-motion: reduce) {
  .accent { background-size: 100% 0.09em; transition: none; }
}

/* ── step ──────────────────────────────────────────────────────────────── */

.step { display: flex; flex-direction: column; gap: var(--s-3); }
.step-index {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--ls-micro);
  color: var(--sage-deep);
}
.step-rule { height: 1px; background: var(--rule-soft); }

/* ── media band ────────────────────────────────────────────────────────────
   Rhode. Full-bleed imagery with the copy held inside --measure-wide. */

.media-band { position: relative; overflow: hidden; background: var(--graphite); }
.media-band video,
.media-band img { width: 100%; height: 100%; object-fit: cover; }
.media-band-inner {
  position: absolute;
  inset: auto 0 0 0;
  padding: var(--s-12) var(--gutter);
  color: var(--on-dark);
  background: linear-gradient(to top, rgba(49, 47, 43, 0.72), transparent);
}
.media-frame {
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-4);
}

/* ── assurance strip ───────────────────────────────────────────────────── */

.assure {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 1024px) { .assure { grid-template-columns: repeat(4, 1fr); } }
.assure-item { display: flex; flex-direction: column; gap: var(--s-2); }
.assure-item strong { font-size: var(--t-sm); }
.assure-item span { font-size: var(--t-caption); color: var(--ink-muted); }

/* ── meters ────────────────────────────────────────────────────────────────
   The free-shipping threshold on the cart, and the loyalty tier bar on the
   account page. Same shape, different data. */

.meter {
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--rule-hair);
  overflow: hidden;
}
.meter-fill {
  height: 100%;
  border-radius: var(--r-pill);
  background: var(--sage);
  transition: width var(--d-slow) var(--ease);
}
.meter-label { font-size: var(--t-caption); color: var(--ink-muted); }

.tier-meter { display: flex; flex-direction: column; gap: var(--s-2); }
.tier-list { display: flex; flex-wrap: wrap; gap: var(--s-2); list-style: none; margin: 0; padding: 0; }

/* ── table ─────────────────────────────────────────────────────────────────
   Utility. Used by the admin console and the shipping rates table. No zebra
   striping — both reference systems name it an anti-pattern, and a 1px rule
   separates rows without painting half of them a different colour. */

.tbl { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
.tbl th,
.tbl td { padding: var(--s-3) var(--s-4); text-align: left; border-bottom: 1px solid var(--rule-soft); }
.tbl th {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  color: var(--ink-muted);
}
.tbl tbody tr:hover { background: var(--mist); }

/* ── sticky add-to-cart ────────────────────────────────────────────────────
   Kylie. PDP only, below 1024, revealed once the buy box scrolls out. */

.atc-bar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: var(--z-stuck);
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-3) var(--gutter);
  padding-bottom: max(var(--s-3), env(safe-area-inset-bottom));
  background: var(--shell);
  border-top: 1px solid var(--rule);
  box-shadow: var(--shadow-2);
  transform: translateY(100%);
  transition: transform var(--d-base) var(--ease);
}
.atc-bar.is-in { transform: none; }
.atc-bar .btn { margin-left: auto; }
@media (min-width: 1024px) { .atc-bar { display: none; } }

/* ── footer ────────────────────────────────────────────────────────────────
   Kylie's four columns, then Rhode's wordmark: the brand set at display size
   and clipped by the bottom edge of the document. The clip is the point — it
   reads as a mark rather than as a heading. */

.footer { background: var(--graphite); color: var(--on-dark); padding-top: var(--section-y); overflow: hidden; }
.footer a { color: var(--on-dark-mute); text-decoration: none; }
.footer a:hover { color: var(--on-dark); }

.footer-cols {
  display: grid;
  gap: var(--s-8);
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 1024px) { .footer-cols { grid-template-columns: repeat(4, 1fr); gap: var(--s-10); } }

.footer-col h3 {
  font-family: var(--font-mono);
  font-variation-settings: normal;
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  color: var(--on-dark-mute);
  margin-bottom: var(--s-4);
}
.footer-col ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
.footer-col li { font-size: var(--t-sm); }

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  justify-content: space-between;
  margin-top: var(--s-12);
  padding-top: var(--s-5);
  border-top: 1px solid rgba(238, 238, 236, 0.16);
  font-size: var(--t-caption);
  color: var(--on-dark-mute);
}

/* The clip is the point — it reads as a mark rather than as a heading — but it
   only ever worked at >=1280px, and below that this was simply broken.

   .t-mega resolves to `clamp(3.5rem, 18vw, 11rem)`, and "Nouri Narie" set in
   Archivo at wght 800 / wdth 88 measures about 5.97x its font size. 18vw is
   therefore always wider than the container it sits in, so the string wrapped to
   two lines — measured at 360, 414, 768 and 1024 — and `line-height: 0.78` plus
   the negative bottom margin then sliced the SECOND line through its middle at
   the document edge. What shipped below 1280 was "NOURI" whole above a bisected
   "NARIE": not a clipped mark, just wrapped text with its lower half cut off.

   white-space: nowrap makes the intent explicit, and the min() caps the fluid arm
   at the width the string can actually occupy. 16vw x 5.97 = 95.5% of the
   viewport, which clears the edge at every width:
     1440 -> min(176, 230) = 176px  (unchanged — desktop is byte-identical)
     1280 -> min(176, 205) = 176px  (unchanged)
     1024 -> min(176, 164) = 164px, text ~979px  < 1024
      390 -> min( 56,  62) =  56px, text ~334px  <  390
      360 -> min( 56,  58) =  56px, text ~334px  <  360
   The 3.5rem lower arm of --t-mega is what binds below ~389px, and it is already
   narrow enough, so the mark stays on one line all the way down.

   COLOUR: --alu-shadow, not --on-dark at 14% opacity. The old pairing composited
   to #4B4A46 against --graphite, which measures 1.51:1 — invisible enough to
   read as a rendering smudge rather than as a mark, and the single largest
   contributor to a Lighthouse colour-contrast score of 0. --alu-shadow is
   already in the palette (it is the sampled shadow side of the real bottle) and
   measures 3.09:1 on --graphite, which clears the 3:1 large-text threshold with
   the mark at 176px/56px. The palette did not need to change; the pairing did.
   Stated as a colour rather than an opacity so the value is auditable with a
   picker instead of having to be composited first. */
.footer-wordmark {
  margin-top: var(--s-10);
  color: var(--alu-shadow);
  text-align: center;
  line-height: 0.78;
  white-space: nowrap;
  font-size: min(var(--t-mega), 16vw);
  /* Clipped by the document edge on purpose. */
  margin-bottom: -0.18em;
  user-select: none;
}
