/* ============================================================
   CAPELY: shared chrome: nav, buttons, sections, footer.
   Section stylesheets must not restyle anything in here.
   ============================================================ */

/* ============================================================
   PAGE TRANSITIONS

   Cross-document view transitions, so moving between the homepage, the shop
   page and a cape's own page is one continuous move rather than a white flash
   and a jump. Browsers without support simply navigate as they always did:
   this is an enhancement with nothing behind it to break.

   js/modules/cape-card.js tags the cape being opened with
   view-transition-name: cape-hero just before the click navigates, and
   css/pages/cape.css gives the destination's stage the same name, so the cape
   itself travels between the two pages instead of dissolving.

   Names must be unique per document. That is why the grid does not carry the
   name statically: nine cards all called cape-hero makes the browser skip the
   transition entirely.
   ============================================================ */
@view-transition { navigation: auto; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .32s;
  animation-timing-function: var(--e-out);
}

/* A page transition is motion, and reduced motion means none of it. Nothing
   here is load bearing, so it is safe to switch the whole mechanism off. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* ---------- layout primitives ---------- */
.container { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gutter); }
.container-mid { max-width: var(--maxw-mid); }
.container-text { max-width: var(--maxw-text); }

.section { position: relative; padding: clamp(72px, 8vw, 112px) 0; }
/* Phones only. The clamp bottoms out at 72px, so a section boundary costs
   144px of empty column on a 390px screen, which is most of a fold spent on
   nothing. Still one uniform rule for every section, just a smaller one. */
@media (max-width: 640px) { .section { padding: 60px 0; } }
.section-head { max-width: var(--maxw-text); margin: 0 auto clamp(36px, 4vw, 52px); text-align: center; }
.section-head h2 { font-size: clamp(28px, 4vw, 40px); font-weight: 700; margin-bottom: 12px; }
.section-head p { color: var(--text-mute); font-size: 15px; }
.accent { color: var(--accent); }

.eyebrow {
  display: inline-block; margin-bottom: 14px;
  font-family: 'Outfit', sans-serif; font-size: 11.5px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--accent);
}

/* ---------- page background ----------
   A 44px grid at low contrast, faded out radially so it reads as texture near
   the top and disappears before it can fight the content. Fixed, so it does not
   parallax against the page and cause repaint work on every scroll frame. */
.bg-grid {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  /* Force its own compositor layer. Both backdrops are static and full
     viewport; promoted, they are rasterised once and translated for free
     while the page scrolls, instead of being repainted underneath it. */
  transform: translateZ(0);
  background-image:
    linear-gradient(to right, color-mix(in srgb, var(--text) 3.5%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in srgb, var(--text) 3.5%, transparent) 1px, transparent 1px);
  background-size: 44px 44px;
  -webkit-mask-image: radial-gradient(ellipse 78% 52% at 50% 0%, var(--base-deep) 0%, transparent 72%);
          mask-image: radial-gradient(ellipse 78% 52% at 50% 0%, var(--base-deep) 0%, transparent 72%);
}
.bg-glow {
  position: fixed; z-index: 0; pointer-events: none;
  width: 900px; height: 620px; left: 50%; top: -280px; transform: translate3d(-50%, 0, 0);
  background: radial-gradient(ellipse at center, color-mix(in srgb, var(--accent) 7.5%, transparent), transparent 66%);
  filter: blur(20px);
}
/* ---------- Minecraft backdrop ----------
   Opt in per page with <html data-bg="night|horizon|cave">. With no attribute
   the site keeps the original grid-and-glow backdrop above, so a variant can be
   looked at without the whole site committing to it.

   Why this is affordable on a page whose first requirement is scroll
   smoothness: every layer is fixed or section-bound, promoted, and completely
   static. A fixed promoted layer is rasterised once and then translated by the
   compositor, so none of it repaints on a scroll frame. Nothing here animates,
   transitions on scroll, or reads the scroll position, and adding something
   that does would undo the whole argument.

   The tiles in assets/bg/ are ALPHA MASKS, not pictures: RGB is white and only
   alpha carries the shape, so the colour still comes from a theme.css token
   through the fill underneath. A mask fill may be a gradient, which is how the
   star field fades down the viewport without a second layer or a
   mask-composite trick. Tiles are generated at the size they are displayed
   (tools/make-bg-tiles.js) and are never scaled here, because a scaled mask is
   smoothed by the compositor and smooth is the opposite of the point.

   A variant is four custom properties and nothing else. That matters because
   the backdrop has to be built TWICE - once as the fixed page layer, once
   inside the hero, for the reason documented at .hero-section below - and two
   hand-written copies of three variants is six things to keep in step. */
html[data-bg="night"],
html[data-bg="horizon"] {
  --mc-back: linear-gradient(180deg, var(--bg-night) 0%, var(--bg-night-2) 38%, var(--base) 74%);
  --mc-ink: linear-gradient(180deg,
    color-mix(in srgb, var(--text) 62%, transparent) 0%,
    color-mix(in srgb, var(--text) 30%, transparent) 42%,
    transparent 76%);
  /* Stars only. clouds.png is generated and available, and it was in this
     mask until it was rendered and looked at: Minecraft's cloud slabs land
     mid-viewport behind the body copy, where flat grey rectangles read as
     broken image placeholders rather than as sky. Re-adding them means
     confining them to the top of the viewport and cutting their baked alpha,
     not just putting the layer back. */
  --mc-mask: url('../assets/bg/stars.png');
  --mc-mask-size: 320px 320px;
  --mc-mask-pos: 0 0;
}
html[data-bg="cave"] {
  --mc-back: radial-gradient(ellipse 130% 90% at 50% 0%, var(--bg-night-2), var(--base) 68%);
  --mc-ink: color-mix(in srgb, var(--text) 11%, transparent);
  --mc-mask: url('../assets/bg/stone.png');
  --mc-mask-size: 64px 64px;
  --mc-mask-pos: 0 0;
}

/* ---- the page layer: everything below the hero ---- */
.bg-mc { position: fixed; inset: 0; z-index: 0; pointer-events: none; display: none; }
html[data-bg] .bg-mc {
  display: block; transform: translateZ(0);
  background: var(--mc-back);
}
html[data-bg] .bg-mc::before {
  content: ''; position: absolute; inset: 0;
  background: var(--mc-ink);
  -webkit-mask-image: var(--mc-mask);          mask-image: var(--mc-mask);
  -webkit-mask-size: var(--mc-mask-size);      mask-size: var(--mc-mask-size);
  -webkit-mask-position: var(--mc-mask-pos);   mask-position: var(--mc-mask-pos);
  -webkit-mask-repeat: repeat;                 mask-repeat: repeat;
}
/* The cave vignette. Only this variant has one, so it stays a plain rule
   rather than becoming a fifth knob that three of four variants set to none. */
html[data-bg="cave"] .bg-mc::after {
  content: ''; position: absolute; inset: 0;
  background: radial-gradient(ellipse 88% 66% at 50% 38%, transparent 42%, var(--base-deep) 100%);
}

/* ---- horizon: a blocky skyline along the bottom of the viewport ----
   One tile, one solid accent fill. The 4px cap is written at high alpha in the
   tile and the mass below it at low, so a single mint fill reads as a lit
   grass edge over dark terrain: the night answer to the bright green strip a
   competitor runs along the bottom of their page. */
html[data-bg="horizon"] .bg-mc::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 96px;
  background: var(--accent);
  -webkit-mask-image: url('../assets/bg/horizon.png');
          mask-image: url('../assets/bg/horizon.png');
  -webkit-mask-size: 512px 96px;               mask-size: 512px 96px;
  -webkit-mask-repeat: repeat-x;               mask-repeat: repeat-x;
  -webkit-mask-position: left bottom;          mask-position: left bottom;
}

/* ---- the hero, which the fixed layer cannot reach ----
   .hero-section paints an OPAQUE radial wash across the whole first viewport,
   and it has to: it carries isolation: isolate so that .hero-stage-bloom's
   screen blend has a real backdrop to cancel the cape's dark pixels against.
   Take that opacity away and the bloom degrades to a grey haze, which this
   project has already fixed once. So the fixed backdrop was invisible for the
   entire first screen - measured, a 1.4% pixel diff of which 96% sat in the
   one band below the hero.

   The fix is to rebuild the same three layers inside the hero's own stacking
   context, in the order the section needs: the opaque backdrop becomes the
   section's own background (so isolation still has something to blend
   against), the per-cape wash moves up onto ::before, and the texture sits on
   ::after. The wash keeps its transition here, or the colour would snap as the
   carousel changes cape. */
html[data-bg] .hero-section { background: var(--mc-back); }
/* Both pseudo-elements sit at z-index -1, and that exact value is load
   bearing. A negative-z-index child paints above its parent's own background
   and below every piece of in-flow content, which is precisely the slot a
   backdrop wants, and it gets there without touching the hero's markup.

   The obvious alternative - leave these at z-index 0 and lift the content with
   .hero-section > * { position: relative; z-index: 1 } - is WRONG here. A
   positioned element with a real z-index creates a stacking context, and a
   stacking context is a blend isolation group, so .hero-stage-bloom's screen
   blend would suddenly be confined to .hero-grid, whose backdrop is
   transparent. That is the grey-haze regression the isolation comment above
   describes, reintroduced by a rule that looks like pure layout. */
html[data-bg] .hero-section::before {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: radial-gradient(ellipse 65% 55% at 50% 40%,
    color-mix(in srgb, var(--cape-accent, var(--accent)) 7%, transparent) 0%, transparent 72%);
  transition: background var(--t-slow) var(--e-out);
}
html[data-bg] .hero-section::after {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: var(--mc-ink);
  -webkit-mask-image: var(--mc-mask);          mask-image: var(--mc-mask);
  -webkit-mask-size: var(--mc-mask-size);      mask-size: var(--mc-mask-size);
  -webkit-mask-position: var(--mc-mask-pos);   mask-position: var(--mc-mask-pos);
  -webkit-mask-repeat: repeat;                 mask-repeat: repeat;
}

main, .nav, .footer { position: relative; z-index: 1; }

/* js/site.js puts this on <html> for as long as the page is moving, and takes
   it off 140ms after the last scroll event. Suppressing hit testing while the
   page slides under a stationary cursor is the single largest scroll saving on
   this page: no :hover flips, so no style recalculation and no hover
   transitions repainting mid-scroll. See initScrollState() for the numbers.
   Scoped to main so the nav stays live throughout. */
.is-scrolling main { pointer-events: none; }

/* ---------- buttons ----------
   Capeify's "Buy Now" / "More Capes" feel (capeify-src/app/page.js ~L516-530),
   ported onto Capely's own .btn/.btn-primary/.btn-ghost recipe, 2026-08-20:
   roomier padding, a 14px (--r-md) radius, a gradient fill on the primary, a
   soft ambient shadow tinted to the button's own colour, a hover lift.

   Colour hook: every fill/border/shadow below reads var(--btn-accent, ...)
   with --accent as the fallback. This file never SETS --btn-accent. It
   exists purely so a caller can tint one button, or a whole subtree (custom
   properties inherit), by setting that one property inline, e.g. the hero
   giving its two CTAs style="--btn-accent: <the current cape's accent>" so
   they follow the cape on screen instead of staying a fixed brand colour. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 44px; padding: 0 24px;
  font-family: 'Outfit', sans-serif; font-size: 14px; font-weight: 600;
  border-radius: var(--r-md); white-space: nowrap; cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast),
    box-shadow var(--t-fast), transform var(--t-fast);
}
.btn:active { transform: translateY(1px); }
.btn-primary {
  background: linear-gradient(135deg, var(--btn-accent, var(--accent)), color-mix(in srgb, var(--btn-accent, var(--accent)) 73%, transparent));
  color: var(--on-accent);
  box-shadow: 0 8px 30px color-mix(in srgb, var(--btn-accent, var(--accent)) 30%, transparent);
}
.btn-primary:hover {
  box-shadow: 0 12px 40px color-mix(in srgb, var(--btn-accent, var(--accent)) 45%, transparent);
  transform: translateY(-2px);
}
/* Arrow affordance: Capeify's "Buy Now" carries a trailing ArrowRight that
   nudges on hover. Nothing ships a .btn-primary icon yet, so this is inert
   scaffolding, ready the moment one does (last child so a leading spinner,
   e.g. .btn-spinner on the checkout pay button, is never mistaken for it). */
.btn-primary svg:last-child { transition: transform var(--t-fast); }
.btn-primary:hover svg:last-child { transform: translateX(2px); }

.btn-ghost {
  background: color-mix(in srgb, var(--btn-accent, var(--accent)) 6%, transparent);
  color: var(--btn-accent, var(--accent));
  border: 1px solid color-mix(in srgb, var(--btn-accent, var(--accent)) 25%, transparent);
}
.btn-ghost:hover {
  border-color: color-mix(in srgb, var(--btn-accent, var(--accent)) 50%, transparent);
  background: color-mix(in srgb, var(--btn-accent, var(--accent)) 12%, transparent);
  box-shadow: 0 4px 20px color-mix(in srgb, var(--btn-accent, var(--accent)) 15%, transparent);
  transform: translateY(-2px);
}
.btn-lg { height: 56px; padding: 0 32px; font-size: 16px; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .45; pointer-events: none; }

/* ---------- the card recipe (contract §3) ---------- */
.card {
  background: var(--card-wash), var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
}
.card-hover { transition: transform var(--t) var(--e-out), border-color var(--t); }
.card-hover:hover { transform: translateY(-3px); border-color: var(--hairline-strong); }

/* ---------- back link ----------
   Used by shop.html and cape.html. Lives here, not in a page stylesheet: it is
   chrome that both deep pages carry, and the first version of it shipped in
   css/pages/shop.css only, so on cape.html it fell back to the global
   `svg { display:block; max-width:100% }` reset and rendered its chevron as a
   700px graphic filling the screen.

   It is a real pill button, not a bare text link. As plain muted text it
   disappeared into the background on both pages and the owner could not find
   it. A back control is the only way out of a deep page, so it has to be
   visible without being hunted for.

   `display: flex` with `width: fit-content`, not inline-flex: a flex container
   is block level, so it takes a line of its own while still shrinking to its
   contents. Inline, it sat on the same line as the eyebrow that follows it on
   the shop page. */
.back-link {
  display: flex;
  align-items: center;
  gap: 7px;
  width: fit-content;
  height: 38px;
  padding: 0 16px 0 13px;
  margin-bottom: 20px;
  border-radius: var(--r-pill);
  background: var(--surface);
  border: 1px solid var(--hairline);
  color: var(--text-dim);
  font-family: 'Outfit', sans-serif;
  font-size: 13.5px;
  font-weight: 600;
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast), transform var(--t-fast);
}
.back-link svg { width: 15px; height: 15px; flex: 0 0 auto; transition: transform var(--t-fast); }
.back-link:hover {
  color: var(--text);
  background: var(--surface-2);
  border-color: var(--hairline-strong);
}
.back-link:hover svg { transform: translateX(-2px); }
.back-link:active { transform: translateY(1px); }

/* ---------- scroll reveal (js/site.js adds .in) ---------- */
.reveal {
  opacity: 0; transform: translateY(14px);
  transition: opacity var(--t-slow) var(--e-out), transform var(--t-slow) var(--e-out);
  transition-delay: calc(var(--i, 0) * 60ms);
}
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}

/* ============================================================
   NAV: a full-width fixed glass bar, ported from Velocity 2026-08-20
   (velocity-web/css/site.css .nav/.nav.scrolled, ~L27-49). Transparent over
   the hero; js/site.js's initNav adds .scrolled once window.scrollY > 12,
   which is the only thing that changes: background, blur and a hairline
   bottom border fade in, nothing moves or resizes.
   Velocity's own scrolled fill is a colour literal at roughly 72% opacity;
   reproduced here as color-mix() of the --nav-fill token instead, so
   nothing outside theme.css defines a colour, per the design contract.
   --nav-fill carries Velocity's own nav colour; see theme.css for the value.
   The island used --base-deep until 2026-08-21 and read too close to the page
   it floats over; opacity is unchanged, so the whole shift is the token.
   ============================================================ */
/* Velocity's nav is a floating rounded island inset from the viewport edges,
   not a full-bleed bar. The outer .nav is only the positioner and stays
   transparent and click-through, so the page scrolls past in the gutter
   beside the island; .nav-inner is the island itself and takes the fill,
   the radius, the hairline and the shadow. */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
  display: flex; justify-content: center;
  padding: 14px var(--gutter) 0;
  pointer-events: none;
}
.nav-inner {
  pointer-events: auto;
  width: 100%; max-width: var(--maxw); height: var(--nav-h);
  margin: 0 auto; padding: 0 12px 0 20px;
  display: flex; align-items: center; gap: 16px;
  border-radius: var(--r-md);
  border: 1px solid var(--hairline);
  background: color-mix(in srgb, var(--nav-fill) 88%, transparent);
  /* 12px, not 18px. A backdrop filter is re-evaluated on every scroll frame
     because its backdrop moves, and the cost scales with the radius. At 88%
     opaque fill the difference between 12 and 18 is not visible; the frame
     time is. */
  -webkit-backdrop-filter: blur(12px) saturate(1.4);
          backdrop-filter: blur(12px) saturate(1.4);
  transition: background var(--t), border-color var(--t), box-shadow var(--t);
}
/* Scrolled: the island firms up rather than appearing from nothing, since
   unlike a full-bleed bar it is always visible against the page. */
.nav.scrolled .nav-inner {
  background: color-mix(in srgb, var(--nav-fill) 96%, transparent);
  border-color: var(--hairline-strong);
  box-shadow: var(--shadow-md);
}

/* Three columns. Brand and the right cluster are both flex:1 with a zero basis
   so they balance each other, which puts the middle group on the bar's true
   centre line whatever its labels say. Sizing the middle group instead would
   only centre it by luck and would drift the moment a label changed. */
.nav-brand { flex: 1 1 0; min-width: 0; display: flex; align-items: center; }
.nav-brand img { height: 26px; width: auto; }
.nav-links { flex: 0 1 auto; display: flex; align-items: center; gap: 2px; }
.nav-link {
  padding: 9px 14px; border-radius: var(--r-sm); font-size: 14px; font-weight: 500;
  color: var(--text-mute); white-space: nowrap;
  transition: color var(--t-fast), background var(--t-fast);
}
.nav-link:hover { color: var(--text); background: var(--surface-2); }
/* Active section: a real treatment, not just a colour swap. Accent ink over
   a soft accent-tinted pill, so it reads unmistakably differently from the
   neutral :hover pill above rather than being "the same thing but lighter". */
.nav-link.is-active {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.nav-link.is-active:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); }

.nav-right { flex: 1 1 0; min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 12px; }
/* ---------- the right cluster: currency, basket, CTA ----------

   THE CTA WAS UNREADABLE and this is why. .btn-primary fills with
   linear-gradient(135deg, accent, color-mix(accent 73%, transparent)), so its
   far end is a 73%-alpha accent. Over a page section that lands on something
   near the accent; over the near-black nav island it composites down to a dark
   muddy green, while the label stays --on-accent, which is a near-black ink.
   Dark ink on a dark fill. On the nav the fill is the solid accent instead,
   the label is 700 rather than 600, and the only tinted thing left is the
   shadow. Scoped to .btn-primary.nav-cta so checkout's ghost variant, which is
   deliberately quiet next to Pay, is untouched. */
.nav-cta { height: 40px; padding: 0 20px; font-size: 14px; }
.btn-primary.nav-cta {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 700;
  letter-spacing: .01em;
  box-shadow: 0 8px 24px -14px var(--glow-mint);
}
.btn-primary.nav-cta:hover {
  background: var(--mint-300);
  box-shadow: 0 12px 30px -14px var(--glow-mint);
}

/* ---------- currency picker ----------
   A glyph trigger and a panel of rows: a boxed symbol, the three-letter code,
   and a check on the one in use. Shape taken from the reference the owner sent
   (2026-09-01); every colour is a Capely token, so it reads as this site's.

   It is hand-built rather than a <select> because a browser draws its own
   option list: no tile, no check, no highlight, and no way to stop Chrome
   painting a focus ring on a control that had only been clicked. js/site.js
   owns the behaviour and the ARIA. */
.nav-currency,
.nav-m-currency > .js-currency { position: relative; }

.currency-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  height: 34px;
  /* A floor, so the control does not resize as the visitor moves between
     a one-glyph symbol and a two-glyph one. */
  min-width: 56px;
  padding: 0 9px;
  border-radius: var(--r-sm);
  border: 1px solid var(--hairline);
  background: var(--surface-2);
  color: var(--text-dim);
  font-family: 'Outfit', sans-serif;
  font-size: 13.5px;
  font-weight: 700;
  cursor: pointer;
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.currency-trigger:hover { color: var(--text); border-color: var(--hairline-strong); }

/* Open is a state, and it says so: the trigger holds the accent while its
   panel is on screen, so the two read as one control. */
.nav-currency.is-open .currency-trigger,
.nav-m-currency .is-open .currency-trigger {
  color: var(--accent);
  border-color: var(--accent-line);
  background: var(--surface-3);
}

.currency-glyph { line-height: 1; }
.currency-chevron {
  display: flex;
  width: 12px;
  height: 12px;
  color: var(--text-mute);
  transition: transform var(--t) var(--e-out), color var(--t-fast);
}
.currency-chevron svg { width: 100%; height: 100%; }
.nav-currency.is-open .currency-chevron,
.nav-m-currency .is-open .currency-chevron {
  transform: rotate(180deg);
  color: var(--accent);
}

.currency-menu {
  position: absolute;
  z-index: 20;
  top: calc(100% + 10px);
  right: 0;
  min-width: 184px;
  list-style: none;
  padding: 6px;
  border-radius: var(--r-md);
  border: 1px solid var(--hairline-strong);
  background: var(--surface);
  box-shadow: var(--shadow-lg);
}
/* An author `display` outranks the UA sheet's [hidden] rule, and this panel
   sets one. Without this it would never close. tools/lint.js checks for it. */
.currency-menu[hidden] { display: none; }

/* The panel drops out of the trigger rather than appearing. transform-origin
   is the top right because that is the corner it hangs from, so it reads as
   unfolding from the button and not as a box arriving from somewhere else.

   pointer-events is part of the state, not decoration: while it is fading out
   it must not eat a click meant for the page beneath it. js/site.js keeps
   `hidden` off until the transition has finished, which is the only reason
   there is anything to fade. */
.currency-menu {
  opacity: 0;
  transform: translateY(-8px) scale(.97);
  transform-origin: top right;
  pointer-events: none;
  transition:
    opacity var(--t) var(--e-out),
    transform var(--t) var(--e-out);
}
.currency-menu.is-open {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

@media (prefers-reduced-motion: reduce) {
  .currency-menu { transition: none; transform: none; }
}

.currency-option {
  display: grid;
  grid-template-columns: 34px 1fr 16px;
  align-items: center;
  gap: 11px;
  padding: 7px 8px;
  border-radius: var(--r-sm);
  cursor: pointer;
  color: var(--text-dim);
  transition: background var(--t-fast), color var(--t-fast);
}
.currency-option:hover { background: var(--surface-2); color: var(--text); }

/* The row in use: accent ink on a soft accent wash, the same treatment the
   active nav link carries, so "selected" means one thing across the bar. */
.currency-option.is-selected {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
}
.currency-option.is-selected:hover {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
}

/* The boxed symbol. Squared off against the pill-ish rest of the bar on
   purpose: it is a swatch, not a button. */
.currency-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 30px;
  border-radius: var(--r-xs);
  border: 1px solid var(--hairline-strong);
  background: var(--base-deep);
  color: var(--text);
  font-family: 'Outfit', sans-serif;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
}
.currency-option.is-selected .currency-tile {
  border-color: var(--accent-line);
  color: var(--accent);
}

.currency-code {
  font-family: 'Outfit', sans-serif;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: .01em;
}

/* Reserved, not toggled with display: the row must not reflow by 16px the
   moment the selection moves. */
.currency-check {
  display: flex;
  width: 16px;
  height: 16px;
  opacity: 0;
  transition: opacity var(--t-fast);
}
.currency-check svg { width: 100%; height: 100%; }
.currency-option.is-selected .currency-check { opacity: 1; }

/* In the drawer the panel has the whole width to open into, and it hangs from
   the left because the control sits at the row's right edge already. */
.nav-m-currency .currency-menu { right: 0; min-width: 200px; }

.nav-basket {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  color: var(--text);
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.nav-basket:hover { color: var(--accent); border-color: var(--accent-line); }
.nav-basket svg { width: 19px; height: 19px; }

/* The count is the only thing on the bar that changes while the visitor is on
   the page, so it is the accent and it sits proud of the button's corner. */
.nav-basket-count {
  position: absolute; top: -7px; right: -7px;
  min-width: 18px; height: 18px; padding: 0 5px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-pill);
  background: var(--accent); color: var(--on-accent);
  font-family: 'Outfit', sans-serif; font-size: 11px; font-weight: 700; line-height: 1;
}
/* An author `display` outranks the UA sheet's [hidden] { display: none }, so
   an empty basket would show a "0" pip forever without this. Same trap as the
   cape zoom overlay in round 4; tools/lint.js checks for it. */
.nav-basket-count[hidden] { display: none; }
.nav-toggle {
  display: none; width: 38px; height: 38px; border-radius: var(--r-sm);
  background: var(--surface-2); border: 1px solid var(--hairline);
  align-items: center; justify-content: center; color: var(--text);
}
.nav-toggle svg { width: 20px; height: 20px; }

/* ---------- mobile drawer ----------
   Flush edge-to-edge under the full-width bar (Velocity's .nav-mobile
   treatment), not inset to a gutter: that inset panel matched the old
   floating island, and the bar above it is full-bleed now. */
/* Hangs below the island, so it clears the island's own 14px top offset and
   height rather than starting under a bar that is no longer full bleed. */
.nav-mobile {
  position: fixed; z-index: 999;
  top: calc(var(--nav-h) + 22px); left: var(--gutter); right: var(--gutter);
  display: none; flex-direction: column; gap: 2px; padding: 12px 12px 14px;
  border-radius: var(--r-md);
  border: 1px solid var(--hairline-strong);
  /* Same token as the island it hangs off. Two different darks stacked 8px
     apart read as a mistake, not as depth. */
  background: color-mix(in srgb, var(--nav-fill) 96%, transparent);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
          backdrop-filter: blur(20px) saturate(1.4);
  box-shadow: var(--shadow-lg);
  transform: translateY(-10px); opacity: 0;
  transition: transform var(--t) var(--e-out), opacity var(--t);
}
.nav-mobile.open { display: flex; transform: none; opacity: 1; }
.nav-mobile a { padding: 12px 14px; border-radius: var(--r-sm); font-size: 15px; font-weight: 500; color: var(--text-dim); }
.nav-mobile a:hover { background: var(--surface-2); color: var(--text); }
.nav-m-div { height: 1px; margin: 8px 10px; background: var(--hairline); }
.nav-mobile a.nav-m-cta { margin-top: 4px; text-align: center; justify-content: center; background: var(--accent); color: var(--on-accent); font-weight: 600; }
.nav-mobile a.nav-m-cta:hover { background: var(--mint-300); color: var(--on-accent); }

/* The drawer carries the two controls the island drops below 900px. The count
   sits inline here rather than pinned to a corner: there is no icon button to
   pin it to, it follows the word "Basket". */
.nav-m-basket { display: flex; align-items: center; gap: 9px; }
.nav-m-basket .nav-basket-count { position: static; }
.nav-m-currency {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 8px 12px; color: var(--text-mute); font-size: 14px;
}

@media (max-width: 900px) {
  .nav-links { display: none; }
  .nav-toggle { display: inline-flex; }
  /* Moves into the drawer rather than being dropped: a six-value select, the
     basket, the CTA and the burger do not fit on a phone bar. */
  .nav-currency { display: none; }
}
/* Below this the bar is brand + basket + burger. The CTA is one tap away in
   the drawer, and the basket is the control that has to stay reachable
   because it is the only one whose state changes. */
@media (max-width: 560px) {
  .nav-cta { display: none; }
}
/* Above the drawer breakpoint the toggle is gone, so an open drawer would
   otherwise be stranded over the page after a resize. */
@media (min-width: 901px) {
  .nav-mobile, .nav-mobile.open { display: none; }
}

/* ---------- accordion (FAQ) ---------- */
.acc-item { border: 1px solid var(--hairline); border-radius: var(--r-md); background: var(--surface); overflow: hidden; transition: border-color var(--t), background var(--t); }
.acc-item + .acc-item { margin-top: 10px; }
.acc-item.open { border-color: var(--hairline-strong); background: var(--surface-2); }
.acc-head { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 18px 20px; text-align: left; font-family: 'Outfit', sans-serif; font-size: 15.5px; font-weight: 600; color: var(--text); }
.acc-ic { flex: 0 0 auto; width: 18px; height: 18px; color: var(--accent); transition: transform var(--t) var(--e-out); }
.acc-item.open .acc-ic { transform: rotate(45deg); }
/* Open and close animate grid-template-rows 0fr -> 1fr, not a max-height.

   A max-height transition has to be given a number, and the number is always
   wrong: JS measured scrollHeight on every toggle, so the panel eased against
   a height it did not have yet, and a resize left the measurement stale. This
   interpolates the row's TRUE height, so the motion is even in both
   directions, the markup is the only source of truth, and no JS measures
   anything. .acc-body-inner needs min-height: 0 or the grid item refuses to
   shrink below its content. */
.acc-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--t-slow) var(--e-out); }
.acc-item.open .acc-body { grid-template-rows: 1fr; }
.acc-body-inner {
  min-height: 0;
  overflow: hidden;
  /* padding-bottom is 0 while closed and eases in only when the row opens. It
     CANNOT be a constant here. box-sizing is border-box (theme.css), so a box
     can never be laid out shorter than its own padding, and min-height: 0 does
     not change that: a permanent `padding-bottom: 20px` made the 0fr track
     resolve to 20px instead of 0, so every CLOSED row carried 20px of dead
     space under its question and the questions read as stuck to the top of
     their cards. Measured 2026-09-08 on the homepage at 1440: grid-template-rows
     computed to "20px", .acc-item 82.8px tall against a 60.8px .acc-head.
     The cape card's Details panel in 03-shop.css already avoids this the same
     way, by keeping its padding-top at 0 until .is-open. */
  padding: 0 20px;
  color: var(--text-dim);
  font-size: 14.5px;
  line-height: 1.7;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity var(--t) var(--e-out), transform var(--t) var(--e-out),
    padding-bottom var(--t-slow) var(--e-out);
}
.acc-item.open .acc-body-inner { opacity: 1; transform: none; padding-bottom: 20px; }
@media (prefers-reduced-motion: reduce) {
  .acc-body, .acc-body-inner { transition: none; }
  .acc-body-inner { transform: none; }
}

/* ---------- footer ---------- */
.footer { border-top: 1px solid var(--hairline); background: var(--base-deep); padding: clamp(44px, 5vw, 64px) 0 28px; }
.footer-grid { display: grid; grid-template-columns: 1.8fr repeat(3, 1fr); gap: 40px; }
@media (max-width: 820px) { .footer-grid { grid-template-columns: repeat(3, 1fr); gap: 30px 20px; } .footer-about { grid-column: 1 / -1; } }
@media (max-width: 520px) { .footer-grid { grid-template-columns: repeat(2, 1fr); } }
.footer-about img { height: 24px; width: auto; margin-bottom: 14px; }
.footer-tag { color: var(--text-mute); font-size: 13.5px; line-height: 1.65; max-width: 300px; }
.footer-col h4 { font-family: 'Outfit', sans-serif; font-size: 11.5px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--text-mute); margin-bottom: 12px; }
.footer-col a { display: block; width: fit-content; color: var(--text-dim); font-size: 14px; padding: 5px 0; transition: color var(--t-fast); }
.footer-col a:hover { color: var(--accent); }
.footer-bottom { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin-top: 40px; padding-top: 22px; border-top: 1px solid var(--hairline); color: var(--text-mute); font-size: 12.5px; }

/* ---------- utility ---------- */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.skip-link { position: absolute; left: -9999px; top: 0; z-index: 2000; padding: 12px 18px; background: var(--accent); color: var(--on-accent); border-radius: 0 0 var(--r-sm) 0; font-weight: 600; }
.skip-link:focus { left: 0; }
