/* ==========================================================================
   Shiny Trading Company — design tokens
   Ink/Paper alternating sections, foil-holo signature, manifest/ledger motif
   ========================================================================== */
:root{
  --ink:        #0D0F12;
  --ink-soft:   #14171D;
  --ink-line:   #262A32;
  --ink-text:   #ECEAE3;
  --ink-dim:    #9AA0AC;

  --paper:      #F6F4EE;
  --paper-soft: #EFEBE0;
  --paper-line: #DAD4C3;
  --paper-text: #17181B;
  --paper-dim:  #6B6A63;

  --gold:       #C9A227;
  --gold-bright:#E4BC3E;

  /* Showcase band — a warm charcoal "shelf" pulled toward the gold hue, so it
     separates from the cool near-black hero instead of blending into it. */
  --shelf:      #1E1913;
  --shelf-line: rgba(201,162,39,.16);
  --shelf-tile: #FAF7F0;
  --violet:     #7B5FFF;
  --teal:       #2FD9C4;
  --red-stamp:  #B43B2F;

  --font-display: "Instrument Serif", Georgia, serif;
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;

  --container: 1180px;
  --radius: 3px;
  --shadow-lg: 0 24px 60px -20px rgba(0,0,0,.45);
  --ease: cubic-bezier(.16,.8,.28,1);
}

*,*::before,*::after{ box-sizing:border-box; }
html{ scroll-behavior:smooth; }
@media (prefers-reduced-motion: reduce){ html{ scroll-behavior:auto; } }

/* `clip`, not `hidden` — setting overflow-x:hidden while overflow-y stays at
   its default computes overflow-y to `auto` per spec (browsers can't mix
   visible with a non-clip overflow value), which silently disables every
   position:sticky element on the page, including the book's pinned section.
   `clip` is explicitly exempt from that auto-promotion, so it blocks the
   off-canvas mobile nav's horizontal overflow without touching the y-axis. */
html{ overflow-x:clip; }
body{
  margin:0;
  overflow-x:clip;   /* the off-canvas mobile nav sits translateX(100%) off
                        the right edge; without this it still registers as
                        scrollable area and the whole page gains a
                        horizontal scrollbar on phones. */
  background:var(--paper);
  color:var(--paper-text);
  font-family:var(--font-body);
  font-size:16px;
  line-height:1.55;
  -webkit-font-smoothing:antialiased;
}

img{ max-width:100%; display:block; }
a{ color:inherit; text-decoration:none; }
ul{ margin:0; padding:0; list-style:none; }
button{ font:inherit; cursor:pointer; }
input,select,textarea{ font:inherit; }

h1,h2,h3,h4{
  font-family:var(--font-display);
  font-weight:400;
  line-height:1.05;
  margin:0;
  letter-spacing:-.01em;
}

.container{ max-width:var(--container); margin:0 auto; padding:0 28px; }
@media (max-width:640px){ .container{ padding:0 20px; } }

section{ position:relative; }
.section-pad{ padding:110px 0; }
@media (max-width:900px){ .section-pad{ padding:76px 0; } }

.bg-ink{ background:var(--ink); color:var(--ink-text); }
.bg-paper{ background:var(--paper); color:var(--paper-text); }
.bg-ink-soft{ background:var(--ink-soft); color:var(--ink-text); }

.mono{ font-family:var(--font-mono); }

:focus-visible{
  outline:2px solid var(--gold-bright);
  outline-offset:3px;
  border-radius:2px;
}

/* ---------- buttons ---------- */
.btn{
  display:inline-flex;
  align-items:center;
  gap:10px;
  padding:15px 26px;
  border-radius:var(--radius);
  font-family:var(--font-mono);
  font-size:13px;
  letter-spacing:.08em;
  text-transform:uppercase;
  border:1px solid transparent;
  transition:transform .35s var(--ease), background .3s, border-color .3s, color .3s;
  white-space:nowrap;
  /* uppercase + letter-spacing leaves a trailing gap after the last glyph,
     which reads as the label sitting left of centre — pull it back by the
     same amount so it is optically centred in the button. */
  justify-content:center;
  text-indent:.08em;
}
.btn:hover{ transform:translateY(-2px); }
.btn-gold{
  background:var(--gold);
  color:#171204;
  font-weight:600;
}
.btn-gold:hover{ background:var(--gold-bright); }
.btn-outline-dark{
  border-color:rgba(236,234,227,.34);
  color:var(--ink-text);
}
.btn-outline-dark:hover{ border-color:var(--gold); color:var(--gold-bright); }
.btn-outline-light{
  border-color:rgba(23,24,27,.28);
  color:var(--paper-text);
}
.btn-outline-light:hover{ border-color:var(--paper-text); }
.btn-block{ width:100%; justify-content:center; }
.btn[disabled]{ opacity:.45; cursor:not-allowed; transform:none !important; }

/* ==========================================================================
   Header
   ========================================================================== */
.site-header{
  position:sticky; top:0; z-index:200;
  background:rgba(13,15,18,.86);
  backdrop-filter:blur(10px);
  border-bottom:1px solid var(--ink-line);
}
/* Homepage: the header floats on the hero instead of sitting in a bar of its
   own — same as corporate.pokemon.co.jp. Fixed (not sticky) so the hero art
   runs all the way up behind it; the hero gets the height back as padding.
   The soft top scrim on ::before is the only thing keeping the logo legible
   over the art, and it lifts once .is-solid takes over (see js/main.js). */
body.home .site-header{
  position:fixed; left:0; right:0; top:0;
  background:transparent;
  backdrop-filter:none;
  border-bottom-color:transparent;
  transition:background .35s ease, backdrop-filter .35s ease, border-color .35s ease;
}
body.home .site-header::before{
  content:""; position:absolute; inset:0; z-index:-1;
  background:linear-gradient(to bottom, rgba(13,15,18,.55), rgba(13,15,18,0));
  transition:opacity .35s ease;
}
body.home .site-header.is-solid{
  background:rgba(13,15,18,.86);
  backdrop-filter:blur(10px);
  border-bottom-color:var(--ink-line);
}
body.home .site-header.is-solid::before{ opacity:0; }
/* The header no longer takes up space in the flow, so the hero carries it.
   The hero itself fills the viewport — full width, full height — with the
   artwork running edge to edge behind it and the copy centred in what is
   left below the header. */
body.home .hero{
  padding-top:172px;
  min-height:100svh;
  display:flex; flex-direction:column; justify-content:center;
}
@media (max-width:760px){
  body.home .hero{ padding-top:132px; }
}
/* An open mobile menu needs the opaque panel behind it either way. */
body.home.nav-open .site-header{
  background:var(--ink); backdrop-filter:none; border-bottom-color:transparent;
}
body.home.nav-open .site-header::before{ opacity:0; }

.header-inner{
  display:flex; align-items:center; justify-content:space-between;
  height:76px;
  gap:24px;
}
.logo{
  font-family:var(--font-display);
  font-size:22px;
  color:var(--ink-text);
  display:flex; align-items:baseline; gap:8px;
  flex-shrink:0;
}
.logo em{
  font-style:italic;
  background:linear-gradient(100deg,var(--gold) 0%,var(--gold-bright) 30%,var(--teal) 55%,var(--violet) 75%,var(--gold) 100%);
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
}
.logo small{
  font-family:var(--font-mono);
  font-size:10px;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--ink-dim);
  font-style:normal;
}
.main-nav{ display:flex; gap:30px; align-items:center; }
.main-nav a{
  font-size:14px;
  color:var(--ink-dim);
  position:relative;
  padding:6px 0;
  transition:color .25s;
}
.main-nav a:hover, .main-nav a.active{ color:var(--ink-text); }
.main-nav a.active::after{
  content:"";
  position:absolute; left:0; right:0; bottom:0;
  height:1px; background:var(--gold);
}
.icon-basket, .icon-login{ display:block; }
.header-tools{ display:flex; align-items:center; gap:18px; flex-shrink:0; }
.lang-switch{ display:flex; gap:2px; font-family:var(--font-mono); font-size:12px; }
.lang-switch button{
  background:none; border:1px solid var(--ink-line); color:var(--ink-dim);
  padding:5px 9px; border-radius:2px;
}
.lang-switch button.active{ color:var(--gold); border-color:var(--gold); }
.header-icon{ position:relative; display:inline-flex; align-items:center; justify-content:center; width:26px; height:28px; color:var(--ink-dim); transition:color .25s; }
.header-icon:hover, .header-icon:focus-visible, .header-icon.active{ color:var(--ink-text); }
.header-icon.active::after{ content:""; position:absolute; left:3px; right:3px; bottom:0; height:1px; background:var(--gold); }
.header-basket .quote-badge{ position:absolute; top:-3px; right:-7px; width:15px; height:15px; font-size:9px; }
.nav-mobile-actions, .nav-mobile-lang{ display:none; }

/* Sign out is an action rather than a destination, so it is a <button> — and
   a button arrives with the browser's own chrome and none of the type the
   links beside it inherit from .main-nav a. These two rules hand it back. */
button.header-icon{ background:none; border:0; padding:0; }
.main-nav button.nav-mobile-action{
  background:none; border:0; border-bottom:1px solid var(--ink-line);
  padding:16px 0; font:inherit; font-size:17px; color:var(--ink-dim);
  text-align:left; transition:color .25s;
}
.main-nav button.nav-mobile-action:hover{ color:var(--ink-text); }
.quote-badge{
  background:var(--gold); color:#171204; font-family:var(--font-mono);
  font-size:10px; font-weight:700; border-radius:50%;
  width:17px; height:17px; display:inline-flex; align-items:center; justify-content:center;
}
.nav-toggle{
  display:none; background:none; border:none; color:var(--ink-text);
  width:32px; height:24px; position:relative;
}
.nav-toggle span, .nav-toggle span::before, .nav-toggle span::after{
  content:""; position:absolute; left:0; right:0; height:1px; background:currentColor;
  transition:transform .3s, opacity .3s;
}
.nav-toggle span{ top:11px; }
.nav-toggle span::before{ top:-8px; }
.nav-toggle span::after{ top:8px; }

@media (max-width:940px){
  .main-nav{
    position:fixed; inset:76px 0 0 0; background:var(--ink);
    flex-direction:column; align-items:stretch; padding:36px 28px; gap:0;
    transform:translateX(100%); transition:transform .4s var(--ease);
    overflow-y:auto;
  }
  .main-nav a{ padding:16px 0; border-bottom:1px solid var(--ink-line); font-size:17px; text-align:left; }
  body.nav-open .main-nav{ transform:translateX(0); }
  .nav-toggle{ display:block; }
  .header-tools .lang-switch{ display:none; }
  .header-icon{ display:none; }
  .nav-mobile-actions{ display:block; margin-top:20px; }
  .main-nav .nav-mobile-action{ display:flex; align-items:center; gap:10px; width:100%; }
  .main-nav .nav-mobile-action .quote-badge{ margin-left:auto; }
  .nav-mobile-lang{ display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:auto; padding-top:28px; color:var(--ink-dim); font-family:var(--font-mono); font-size:11px; letter-spacing:.12em; text-transform:uppercase; }
  .nav-mobile-lang .lang-switch{ display:flex; }
  /* backdrop-filter creates a containing block for fixed descendants in
     Chromium, which would confine .main-nav's fixed overlay to the header's
     own box instead of the viewport — drop it while the menu is open. */
  body.nav-open .site-header{ backdrop-filter:none; background:var(--ink); }
}
@media (max-width:480px){
  /* "— Wholesale" subtitle and the header gap are the two things that push
     a narrow phone header past its available width. */
  .logo small{ display:none; }
  .header-inner{ gap:14px; }
  .logo{ font-size:19px; }
}

/* ==========================================================================
   Hero + foil signature card
   ========================================================================== */
.hero{
  padding:96px 0 110px;
  position:relative;
  overflow:hidden;
}
.hero::before{
  content:"闪 亮 贸 易";
  position:absolute; top:-40px; right:-20px;
  /* Instrument Serif carries no CJK, so this always rendered in a system
     fallback anyway — but naming the web font made the browser re-measure
     this ~1900px-wide block when the font finally swapped in, and that was
     the page's entire CLS (0.125 on desktop). Naming the fallbacks directly
     is visually identical and takes the swap out of the equation. */
  font-family:"Songti SC","STSong","Noto Serif SC","Source Han Serif SC",serif;
  font-size:min(30vw,420px);
  color:rgba(255,255,255,.025);
  letter-spacing:.05em;
  pointer-events:none;
  white-space:nowrap;
}
/* Manga backdrop -----------------------------------------------------------
   img/hero-manga.webp is the scan, already grayscale + inverted at build time
   (so no per-frame CSS filter on a full-bleed layer). Two stacked layers:
   the art itself, and a scrim on ::after that lays a grey wash over the top
   edge and fades the whole thing back into --ink towards the bottom, so the
   headline, the card scene and the scroll cue all keep their contrast.
   Opacity is deliberately low — this is texture, not an image. */
.hero-bg{
  position:absolute; inset:0; z-index:0;
  pointer-events:none;
  background:url("../img/hero-manga.webp") center 12% / cover no-repeat;
  opacity:.16;
  /* Feather the edges so the panel borders never end in a hard cut. */
  -webkit-mask-image:linear-gradient(to bottom, rgba(0,0,0,.55) 0%, #000 26%, rgba(0,0,0,.7) 62%, transparent 96%);
          mask-image:linear-gradient(to bottom, rgba(0,0,0,.55) 0%, #000 26%, rgba(0,0,0,.7) 62%, transparent 96%);
}
/* The scan is full-bleed but sits at 16% opacity behind everything — at phone
   widths the 1500px master is ~300 KB spent on texture nobody can resolve.
   A 900px cut is indistinguishable there and costs ~180 KB less. A background
   image inside a non-matching media query is never fetched, so only one of
   the two is ever downloaded. */
@media (max-width:820px){
  .hero-bg{ background-image:url("../img/hero-manga-900.webp"); }
}
/* Scrim is a sibling of the art, not a child of it — the mask above would
   otherwise eat the grey band at the very top along with the linework. */
.hero::after{
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none;
  background:
    linear-gradient(to bottom,
      rgba(168,174,186,.40) 0%,
      rgba(160,166,178,.18) 16%,
      rgba(13,15,18,0) 42%,
      rgba(13,15,18,.55) 82%,
      var(--ink) 100%);
}
@media (max-width:760px){
  /* Phone: no card scene competing for the screen, so the manga becomes the
     hero image itself — full-bleed, edge to edge, running right up under the
     logo. Same idea as corporate.pokemon.co.jp's mobile hero. */
  .hero-bg{
    opacity:.22;
    /* Panned to the right-hand page so the spread's centre gutter doesn't run
       straight down the middle of the screen; lands on the Pikachu panel. */
    background-position:76% top;
    background-size:cover;
    /* Only the bottom is feathered now; the top has to reach the very edge. */
    -webkit-mask-image:linear-gradient(to bottom, #000 0%, #000 58%, rgba(0,0,0,.6) 82%, transparent 100%);
            mask-image:linear-gradient(to bottom, #000 0%, #000 58%, rgba(0,0,0,.6) 82%, transparent 100%);
  }
  /* Drop the grey wash at the top — on the phone it read as a bar sitting on
     the art instead of as part of it. Only the fade down into --ink stays. */
  .hero::after{
    background:linear-gradient(to bottom,
      rgba(13,15,18,.35) 0%,
      rgba(13,15,18,.10) 22%,
      rgba(13,15,18,0) 45%,
      rgba(13,15,18,.6) 84%,
      var(--ink) 100%);
  }
  /* The three-card scene is a desktop element; hidden here so the artwork
     gets the full width. It stays in the DOM (aria-hidden already) — no JS
     changes needed. */
  body.home .hero-card-stage{ display:none; }
  body.home .hero-grid{ grid-template-columns:1fr; }
  /* Height/centring are inherited from the desktop rule above; with the
     cards gone the artwork simply gets the whole screen to itself. */
}
@media (prefers-reduced-transparency:reduce){
  .hero-bg{ opacity:.08; }
}
.hero-grid{
  display:grid; grid-template-columns:1.05fr .95fr; gap:60px; align-items:center;
  position:relative; z-index:1;
}
.hero h1{
  /* Sized so the headline always wraps over max 2 lines, from narrow phones
     up to the 1180px container, and stays clearly larger than the lead. */
  font-size:clamp(31px,4.6vw,58px);
  text-wrap:balance;
  margin:0 0 18px;
}
.hero-lead{
  font-size:clamp(15px,1.5vw,19px);
  line-height:1.55;
  max-width:46ch;
  color:var(--ink-dim);
  margin:0 0 36px;
}
.hero h1 i:not(.shine){ font-style:italic; color:var(--gold-bright); }
.hero h1 i.shine{ font-style:italic; }

@media (max-width:640px){
  /* Phones only. With the card scene gone the headline is the hero, so it is
     sized to act like one instead of like slightly-large body copy: it scales
     with the viewport (11.4vw ≈ 43px on a 375px phone, against 31px before),
     the leading is pulled in tight so the two or three lines read as one
     block, and a hair of negative tracking keeps the display face from
     looking loose at that size. */
  .hero h1{
    /* 13.4vw ≈ 50px on a 375px phone. With the card scene gone the headline
       carries the whole screen, so it is sized to fill the measure rather
       than sit politely inside it; the tighter leading and tracking keep the
       extra size from turning three lines into a wall. */
    font-size:clamp(44px, 13.4vw, 62px);
    line-height:.98;
    letter-spacing:-.022em;
    margin-bottom:22px;
  }
  /* The lead has to stay clearly subordinate now that the headline is half
     again as big, so it gives up a little size and takes a little more air. */
  .hero-lead{
    font-size:16px;
    line-height:1.6;
    margin-bottom:32px;
  }
}

/* Gold shine sweeping left→right across a word, then resting before it repeats.
   Tune the cycle with --shine-cycle; the sweep itself is ~30% of that. */
.shine{
  color:var(--gold-bright);
  --shine-cycle:5s; /* full cycle: ~1.5s sweep + ~3.1s rest */
}
@supports ((-webkit-background-clip:text) or (background-clip:text)){
  .shine{
    background-image:linear-gradient(100deg,
      var(--gold) 0%,   var(--gold) 40%,
      #fff3cd 47%,      #ffffff 50%,      #fff3cd 53%,
      var(--gold) 60%,  var(--gold) 100%);
    background-size:250% 100%;
    background-repeat:no-repeat;
    -webkit-background-clip:text;
    background-clip:text;
    color:transparent;
    animation:shineSweep var(--shine-cycle) ease-in-out infinite;
  }
}
/* Both end positions land on flat gold, so the loop restart is invisible. */
@keyframes shineSweep{
  0%,   8%  { background-position:100% 0; }
  38%, 100% { background-position:0% 0; }
}
@media (prefers-reduced-motion: reduce){
  .shine{ animation:none; }
}
/* Grid rather than flex so both buttons take the width of the widest label
   instead of each sizing to its own text. inline-grid + max-content keeps the
   pair shrink-wrapped around the labels; 1fr then equalises the columns. */
.hero-actions{
  display:inline-grid;
  grid-auto-flow:column;
  grid-auto-columns:1fr;
  width:max-content;
  max-width:100%;
  gap:16px;
  margin-bottom:44px;
}
@media (max-width:560px){
  /* Stacked, full width — same reason as the closing CTA below. */
  .hero-actions{ grid-auto-flow:row; width:100%; }
}
/* no trailing gap when the stats row below it isn't present */
.hero-actions:last-child{ margin-bottom:0; }
.hero-meta{
  display:flex; gap:34px; flex-wrap:wrap;
  font-family:var(--font-mono); font-size:12px; color:var(--ink-dim);
  border-top:1px solid var(--ink-line); padding-top:22px;
}
.hero-meta b{ display:block; color:var(--ink-text); font-size:20px; font-family:var(--font-body); font-weight:600; }

/* the foil card */
.foil-stage{ perspective:1400px; display:flex; justify-content:center; }
.foil-card{
  width:min(340px,80vw); aspect-ratio:63/88;
  border-radius:16px;
  position:relative;
  background:
    radial-gradient(120% 140% at var(--mx,50%) var(--my,30%),
      rgba(255,255,255,.55), transparent 45%),
    linear-gradient(135deg,#1a1c22,#0a0b0d 60%);
  border:1px solid rgba(255,255,255,.12);
  box-shadow:var(--shadow-lg);
  transform-style:preserve-3d;
  transition:transform .25s var(--ease);
  overflow:hidden;
}
.foil-card::before{
  content:"";
  position:absolute; inset:0;
  background:conic-gradient(from var(--rot,180deg) at var(--mx,50%) var(--my,50%),
    var(--gold), var(--teal), var(--violet), var(--gold-bright), var(--gold));
  mix-blend-mode:screen;
  opacity:.5;
}
.foil-card::after{
  content:"";
  position:absolute; inset:0;
  background:linear-gradient(115deg, transparent 20%, rgba(255,255,255,.5) 36%, transparent 46%);
  background-size:220% 220%;
  background-position:calc(var(--mx,50%) * 1.4) calc(var(--my,50%) * 1.4);
  mix-blend-mode:overlay;
}
.foil-face{
  position:absolute; inset:14px;
  border:1px solid rgba(255,255,255,.5);
  border-radius:10px;
  display:flex; flex-direction:column; justify-content:space-between;
  padding:18px;
  z-index:2;
  pointer-events:none;
}
.foil-face-top{ display:flex; justify-content:space-between; align-items:flex-start; }
.foil-face-top span{ font-family:var(--font-mono); font-size:10px; letter-spacing:.1em; color:rgba(255,255,255,.85); text-transform:uppercase; }
.foil-face-mark{ font-family:var(--font-display); font-style:italic; font-size:15px; color:#fff; }
.foil-face-center{ text-align:center; }
.foil-face-center .cn{ font-size:44px; color:rgba(255,255,255,.92); font-family:var(--font-display); }
.foil-face-center small{ display:block; font-family:var(--font-mono); font-size:10px; letter-spacing:.16em; color:rgba(255,255,255,.6); margin-top:6px; text-transform:uppercase; }
.foil-face-bottom{ display:flex; justify-content:space-between; font-family:var(--font-mono); font-size:9px; color:rgba(255,255,255,.6); letter-spacing:.06em; }
@media (prefers-reduced-motion: reduce){
  .foil-card{ transition:none; }
}
@media (max-width:940px){
  .hero-grid{ grid-template-columns:1fr; }
  .hero{ padding:56px 0 70px; }
  .hero-meta{ gap:22px; }
}
/* Foil card stays for tablet/desktop; on phones it's dropped entirely (not
   just visually hidden) so its layout weight and animation listeners don't
   cost anything on the smallest, most bandwidth-sensitive viewports. */
@media (max-width:767px){
  .foil-stage{ display:none; }
}

/* ==========================================================================
   Hero card scene — 3-card layered TCG composition, GSAP-driven.
   Built from HERO_CARDS in js/main.js §5b. Static layout (position, size,
   base rotation) lives here as inline custom props set by JS; GSAP only
   ever animates transform/opacity on top of that resting layout, via
   quickTo() for pointer parallax+tilt and gsap.to() for hover/intro/idle.
   ========================================================================== */
.hero-card-stage{
  position:relative;
  height:clamp(360px, 44vw, 520px);
  perspective:1700px;
  perspective-origin:50% 42%;
}
/* One soft backlight behind the whole cluster (as in the owner's mockup) —
   huge radius, long falloff, so it reads as ambient glow with no visible
   edge. It sits behind the stack; it is NOT the shine effect itself. */
/* Disabled: the radial gradient never fully reached transparent before
   hitting its own box edge, so it always rendered as a visible rectangle
   behind the cards no matter how soft the falloff — the exact "box" the
   owner flagged. The element/JS wiring (--lx/--ly) is left in place should
   a genuinely edgeless glow be wanted later; see the site memory. */
.card-light{
  position:absolute; inset:-30%;
  background:none;
  pointer-events:none;
  z-index:0;
}
.card-scene{
  position:absolute; inset:0;
  transform-style:preserve-3d;
}
.hero-card{
  position:absolute;
  top:var(--card-top,50%); left:var(--card-left,50%);
  width:var(--card-w,220px);
  transform-style:preserve-3d;
  will-change:transform;
}
/* Middle wrapper: carries the perpetual float so it can never conflict with
   the pointer parallax (outer .hero-card) or the hover scale (inner shell). */
.hero-card .card-float{
  transform-style:preserve-3d;
  will-change:transform;
}
.hero-card .card-shell{
  position:relative;
  width:100%; aspect-ratio:63/88;
  border-radius:5% / 3.6%;
  overflow:hidden;
  transform-style:preserve-3d;
  box-shadow:
    0 2px 0 rgba(255,255,255,.5) inset,
    0 26px 50px -18px rgba(0,0,0,.65),
    0 10px 22px -10px rgba(0,0,0,.55);
  background:#0a0b0d;
}
.hero-card img{
  width:100%; height:100%; object-fit:cover; display:block;
  -webkit-user-drag:none; user-select:none;
}
/* The shine, per card: a light band that sweeps across each card's face on
   its own slow loop (staggered per card via --sheen-delay), exactly like
   light travelling over real foil. Reuses the proven swatchSheen keyframes. */
.hero-card .card-foil{
  position:absolute; inset:0;
  background:linear-gradient(115deg, transparent 32%, rgba(255,255,255,.42) 46%, transparent 60%);
  background-size:250% 250%;
  mix-blend-mode:overlay;
  opacity:.75;
  pointer-events:none;
  animation:swatchSheen 8s ease-in-out infinite;
  animation-delay:var(--sheen-delay,0s);
}
/* Cursor-driven glare on top of the sweep — fades in only while the pointer
   is inside the stage, and follows it, so interacting adds light instead of
   switching to a different effect. */
.hero-card .card-glare{
  position:absolute; inset:0;
  background:radial-gradient(65% 65% at var(--fx,50%) var(--fy,50%), rgba(255,255,255,.32), transparent 72%);
  mix-blend-mode:overlay;
  opacity:0;
  transition:opacity .4s var(--ease);
  pointer-events:none;
}
.hero-card-stage:hover .card-glare{ opacity:.85; }
.hero-card .card-edge{
  position:absolute; inset:0; border-radius:inherit;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.16);
  pointer-events:none;
}
@media (prefers-reduced-motion: reduce){
  .hero-card, .hero-card *{ transition:none !important; }
  .hero-card .card-foil{ animation:none; }
}

/* ==========================================================================
   Product showcase — a slow imagery-only marquee between hero and book.
   Tiles use margin-right (not gap) so the duplicated list loops seamlessly
   at exactly translateX(-50%).
   ========================================================================== */
.showcase{
  position:relative;
  background:var(--shelf);
  border-top:1px solid var(--shelf-line);
  border-bottom:1px solid var(--shelf-line);
  padding:52px 0;
  overflow-x:auto;
  overflow-y:hidden;
  scrollbar-width:none;
  cursor:grab;
  user-select:none;
  /* pan-x as well as pan-y: without it the browser refuses horizontal touch
     scrolling here, so on phones the carousel could not be swiped at all. */
  touch-action:pan-x pan-y;
  -webkit-overflow-scrolling:touch;
  overscroll-behavior-x:contain;
}
.showcase::-webkit-scrollbar{ display:none; }
.showcase.is-dragging{ cursor:grabbing; }
.showcase-track{
  display:flex;
  width:max-content;
}

.showcase-item{
  flex:0 0 auto;
  width:clamp(160px,15vw,210px);
  aspect-ratio:4/5;
  margin-right:20px;
  border:1px solid rgba(255,255,255,.08);
  border-radius:6px;
  /* Warm off-white rather than pure white — echoes the paper sections and
     stops the tiles reading as harsh cut-outs against the dark band. */
  background:var(--shelf-tile);
  box-shadow:0 10px 26px -14px rgba(0,0,0,.7);
  display:flex; align-items:center; justify-content:center;
  position:relative; overflow:hidden;
  transition:transform .45s var(--ease), border-color .3s, box-shadow .45s var(--ease);
}
.showcase-item:hover,
.showcase-item:focus-visible{
  transform:translateY(-6px);
  border-color:var(--gold);
  box-shadow:0 18px 40px -18px rgba(0,0,0,.7);
}
/* Images are background-cropped in JS before they land here, so contain now
   sizes the product itself rather than the product plus its baked-in margin. */
.showcase-item img{
  width:100%; height:100%; object-fit:contain; padding:16px;
  transition:transform .5s var(--ease);
}
.showcase-item:hover img{ transform:scale(1.05); }
@media (prefers-reduced-motion: reduce){
  .showcase{ scroll-behavior:auto; }
}
@media (max-width:640px){
  /* Smaller tiles + tighter padding — roughly 23% shorter band overall
     (304px -> ~235px) so it doesn't dominate the screen on a phone. */
  .showcase{ padding:40px 0; }
  .showcase-item{ width:124px; margin-right:14px; }
  .showcase-item img{ padding:11px; }
}

/* ==========================================================================
   Book — scroll-driven page turn through the five sourcing steps.
   Each page owns a --page-color; JS drives rotateY from the scroll position.
   ========================================================================== */
.book{
  position:relative;
  height:500vh;                       /* 100vh sticky + 4 turns x 100vh */
  background:var(--paper);
  border-top:1px solid var(--paper-line);
  border-bottom:1px solid var(--paper-line);
}
.book-sticky{
  position:sticky; top:0;
  height:100vh;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:clamp(20px,3.4vh,38px);
  overflow:hidden;
}
/* Heading and progress sit above the stage so a lifted, turning sheet
   passes behind them instead of clipping over the type. */
.book-head{ text-align:center; position:relative; z-index:70; }
.book-head h2{ font-size:clamp(26px,3.2vw,40px); }

.book-stage{
  position:relative;
  width:min(760px,88vw);
  height:min(430px,50vh);
  perspective:1900px;
  perspective-origin:56% 50%;
}
/* binding shadow down the spine edge */
.book-spine{
  position:absolute; top:-6px; bottom:-6px; left:-6px; width:30px;
  background:linear-gradient(to right, rgba(13,15,18,.26), rgba(13,15,18,0));
  border-radius:6px 0 0 6px;
  z-index:60; pointer-events:none;
}
.book-page{
  position:absolute; inset:0;
  transform-origin:left center;
  transform-style:preserve-3d;
  will-change:transform, opacity;
}
.page-face{
  position:absolute; inset:0;
  border-radius:3px 12px 12px 3px;
  backface-visibility:hidden;
  overflow:hidden;
  box-shadow:0 30px 70px -28px rgba(13,15,18,.55), 0 2px 6px rgba(13,15,18,.14);
}
.page-front{ background:var(--page-color); color:var(--ink); }
/* the reverse of the sheet — same stock, turned away from the light */
.page-back{
  transform:rotateY(180deg);
  background:var(--page-color);
  border-radius:12px 3px 3px 12px;
}
.page-back::after{
  content:""; position:absolute; inset:0;
  background:linear-gradient(to left, rgba(13,15,18,.55), rgba(13,15,18,.30));
}
.page-inner{
  position:relative; z-index:3;
  height:100%;
  padding:clamp(24px,3.6vw,44px);
  display:flex; flex-direction:column; justify-content:space-between; gap:16px;
}
.page-num, .page-tag{
  font-family:var(--font-mono); font-size:11px;
  letter-spacing:.16em; text-transform:uppercase;
  color:rgba(13,15,18,.62);
}
.page-inner h3{ font-size:clamp(24px,3.2vw,38px); max-width:15ch; }
.page-inner p{
  font-size:clamp(14px,1.25vw,17px);
  /* The 55% is the desktop half of the same text-safe-zone problem the
     max-width:900px block below solves for small screens, and it is needed
     for the same reason: the copy sizes in ch while .page-mon sizes off the
     stage height (--mon-h of min(430px,50vh)), so on a tall desktop window
     the art grows left while the text column does not move, and they meet.
     Measured at 1440x1000 (the worst case, where the stage height caps at
     430px): the art's left edge lands between 57.2% and 66.8% of the stage
     depending on the page, while 44ch of copy reached 67.9%. 55% of the
     content box puts the text's right edge at 54.5% of the stage, clearing
     the widest character by ~21px. The art files have no transparent margin
     — checked, every one is opaque edge to edge — so the box really is the
     visible extent and there is no hidden slack to lean on. */
  max-width:min(44ch, 55%);
  margin:12px 0 0; color:rgba(13,15,18,.78);
}
@media (max-width:900px){
  /* Below this width the ch-based text column and the height-based
     character art size independently and start to overlap (measured up to
     ~176px at 430px wide, on every page except Piplup). Capping both to
     fixed percentage lanes — paired with .page-mon's max-width below —
     means neither can ever grow into the other's space, regardless of
     line length or a character's silhouette. Desktop (>900px) is untouched. */
  .page-inner p{ max-width:min(44ch, 60%); }
}
/* oversized folio number, bled off the outer corner */
.page-big{
  position:absolute; right:-.06em; bottom:-.30em;
  font-family:var(--font-display); font-size:clamp(200px,26vw,330px);
  line-height:.8; color:rgba(13,15,18,.11);
  z-index:1; pointer-events:none; user-select:none;
}
/* spine shadow + outer-edge highlight; opacity is driven by the turn */
/* Character art, paired to each page's colour. Sits above the folio number
   but below the copy, so text always stays readable over it. Because it
   lives inside .page-front it inherits the 3D turn and backface-visibility,
   so it swings away and vanishes with its own page. */
.page-mon{
  position:absolute;
  right:var(--mon-right, 5%);
  bottom:var(--mon-bottom, 0%);
  height:var(--mon-h, 64%);
  width:auto;
  z-index:2;
  pointer-events:none;
  user-select:none;
  filter:drop-shadow(0 14px 20px rgba(13,15,18,.32));
}
@media (max-width:900px){
  /* The text-safe-zone counterpart to the .page-inner p rule above: capping
     max-width forces the browser to also shrink the used height to keep the
     artwork's own aspect ratio (object-fit:contain, not a plain resize) —
     without object-fit the box would still be 34% wide but the *art* inside
     it would squish, since height stays pinned by --mon-h while width gets
     clamped. bottom:16% lifts it clear of the .page-tag caption sitting in
     the same corner, which the old fixed per-page --mon-bottom values were
     never guaranteed to clear at this size. */
  .page-mon{ max-width:34%; bottom:16%; object-fit:contain; }
}
@media (max-width:640px){
  /* Smaller still and pushed to the corner on true phones. */
  .page-mon{ height:calc(var(--mon-h, 64%) * .72); right:2%; opacity:.92; }
}

.page-shade{
  position:absolute; inset:0; z-index:4; pointer-events:none; opacity:0;
  background:
    linear-gradient(to right, rgba(13,15,18,.46) 0%, rgba(13,15,18,0) 30%),
    linear-gradient(to left,  rgba(255,255,255,.34) 0%, rgba(255,255,255,0) 20%);
}

.book-progress{ display:flex; gap:7px; position:relative; z-index:70; }
.book-progress span{
  width:30px; height:3px; border-radius:2px;
  background:rgba(13,15,18,.16);
  transition:background .35s var(--ease), transform .35s var(--ease);
}
.book-progress span.on{ background:var(--ink); transform:scaleX(1.12); }

@media (max-width:760px){
  .book{ height:420vh; }
  .book-stage{ width:92vw; height:min(400px,52vh); perspective:1200px; }
  .page-big{ font-size:clamp(150px,40vw,240px); }
}

/* --------------------------------------------------------------------------
   Card rain — atmospheric background layer for the book section.
   A fixed pool of card backs falls behind the turning pages for the whole
   five-step sequence. Purely decorative: no pointer events, and it sits
   below .book-stage in the stacking order (the stage comes later in the DOM
   and this layer is pinned to z-index 0), so a page never passes behind it.
   Elements are moved with transform only; JS never touches layout props.
   -------------------------------------------------------------------------- */
.card-rain{
  position:absolute; inset:0;
  z-index:0;
  pointer-events:none;
  overflow:hidden;
  contain:layout paint;               /* the falling cards can't reflow anything */
}
.card-rain img{
  position:absolute;
  top:0; left:0;
  width:var(--card-w, 70px);
  height:auto;
  opacity:0;                          /* JS fades each card in on its first fall */
  will-change:transform, opacity;
  transform:translate3d(-999px,-999px,0);
  user-select:none;
}
/* Depth: the furthest lane loses a touch of focus. Kept to a 1px radius and
   only on the back lane, so we're never compositing a heavy blur across many
   elements at once. */
.card-rain img[data-lane="back"]{ filter:blur(1px); }

/* The static (reduced-motion) book has no atmosphere to sit behind. */
.book--static .card-rain{ display:none; }

@media (prefers-reduced-motion: reduce){
  .card-rain{ display:none; }
}

/* No pinning without motion: the pages become a plain stack of colour cards. */
.book--static{ height:auto; }
.book--static .book-sticky{ position:static; height:auto; overflow:visible; padding:70px 0; }
.book--static .book-stage{
  width:min(760px,88vw); height:auto; perspective:none;
  display:flex; flex-direction:column; gap:18px;
}
.book--static .book-page{ position:relative; inset:auto; transform:none !important; opacity:1 !important; }
.book--static .page-face{ position:relative; inset:auto; min-height:240px; }
.book--static .page-back,
.book--static .book-spine,
.book--static .book-progress{ display:none; }
.book--static .page-shade{ display:none; }

/* ==========================================================================
   Section headers
   ========================================================================== */
.sec-head{ display:flex; justify-content:space-between; align-items:flex-end; gap:30px; margin-bottom:52px; flex-wrap:wrap; }
.sec-head h2{ font-size:clamp(30px,3.6vw,44px); }
.sec-head p{ max-width:40ch; color:var(--paper-dim); margin:14px 0 0; }
.bg-ink .sec-head p{ color:var(--ink-dim); }

/* ==========================================================================
   Category / product cards
   ========================================================================== */
.cat-grid{ display:grid; grid-template-columns:repeat(4,1fr); gap:22px; }
@media (max-width:980px){ .cat-grid{ grid-template-columns:repeat(2,1fr); } }
@media (max-width:560px){ .cat-grid{ grid-template-columns:1fr; } }

.cat-card{
  background:var(--ink-soft);
  border:1px solid var(--ink-line);
  border-radius:var(--radius);
  padding:26px 22px 22px;
  position:relative;
  overflow:hidden;
  transition:border-color .3s, transform .3s var(--ease);
}
.cat-card:hover{ border-color:var(--gold); transform:translateY(-4px); }
.cat-card::before{
  content:"";
  position:absolute; top:0; left:0; width:0; height:2px;
  background:linear-gradient(90deg,var(--gold),var(--teal),var(--violet));
  transition:width .5s var(--ease);
}
.cat-card:hover::before{ width:100%; }
.cat-swatch{
  width:100%; aspect-ratio:4/3; border-radius:2px; margin-bottom:18px;
  position:relative; overflow:hidden;
  background:linear-gradient(150deg,var(--swatch-a,#26170a),var(--swatch-b,#0d0f12));
  display:flex; align-items:center; justify-content:center;
}
.cat-swatch::after{
  content:"";
  position:absolute; inset:0;
  background:linear-gradient(120deg,transparent 30%, rgba(255,255,255,.16) 46%, transparent 60%);
  background-size:250% 250%;
  animation:swatchSheen 7s ease-in-out infinite;
}
@keyframes swatchSheen{
  0%,100%{ background-position:0% 0%; }
  50%{ background-position:100% 100%; }
}
.cat-swatch .glyph{ font-family:var(--font-display); font-style:italic; font-size:34px; color:rgba(255,255,255,.75); position:relative; z-index:1; }
.cat-card h3{ font-size:19px; color:var(--ink-text); margin-bottom:6px; }
.cat-card p{ font-size:13px; color:var(--ink-dim); margin:0 0 14px; }
.cat-card .cat-link{ font-family:var(--font-mono); font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:var(--gold); }

/* product grid (shop page) */
.shop-toolbar{
  display:flex; justify-content:flex-start; align-items:flex-start;
  gap:12px 16px; flex-wrap:wrap; margin-bottom:40px;
}

/* Group switch — the primary split between sealed product and singles. A
   segmented control rather than two more dropdown entries, because it is the
   one choice that changes what the sub-filter below it even means. */
.group-switch{
  display:inline-flex; align-items:stretch;
  border:1px solid var(--paper-line); border-radius:20px;
  background:#fff; padding:3px; gap:2px;
}
.group-btn{
  display:inline-flex; align-items:center; gap:7px;
  font-family:var(--font-mono); font-size:12px; text-transform:uppercase; letter-spacing:.05em;
  padding:7px 14px; border:none; border-radius:16px;
  background:none; color:var(--paper-dim); cursor:pointer;
  transition:background .25s, color .25s;
  white-space:nowrap;
}
.group-btn:hover{ color:var(--paper-text); }
.group-btn .count{ font-size:10px; opacity:.7; }
.group-btn.active{ background:var(--ink); color:var(--gold-bright); }
.group-btn.active .count{ opacity:.85; }

/* Category filter — a single dropdown rather than a chip per category, so
   the toolbar isn't a wide row of six competing buttons. */
.filter-dropdown{ position:relative; }
.filter-dropdown-btn{
  display:flex; align-items:center; gap:8px;
  font-family:var(--font-mono); font-size:12px; text-transform:uppercase; letter-spacing:.05em;
  padding:9px 14px; border:1px solid var(--paper-line); border-radius:20px; color:var(--paper-text);
  background:#fff; transition:border-color .25s;
}
.filter-dropdown-btn:hover, .filter-dropdown.open .filter-dropdown-btn{ border-color:var(--paper-text); }
.filter-dropdown-btn span:first-child{ color:var(--paper-dim); }
.filter-dropdown-value{ font-weight:600; }
.filter-dropdown-btn .chevron{ transition:transform .25s; color:var(--paper-dim); }
.filter-dropdown.open .filter-dropdown-btn .chevron{ transform:rotate(180deg); }

.filter-dropdown-menu{
  position:absolute; top:calc(100% + 8px); left:0; z-index:20;
  min-width:220px; background:#fff; border:1px solid var(--paper-line); border-radius:8px;
  box-shadow:var(--shadow-lg); padding:6px;
  opacity:0; visibility:hidden; transform:translateY(-6px);
  transition:opacity .2s var(--ease), transform .2s var(--ease), visibility .2s;
}
.filter-dropdown.open .filter-dropdown-menu{ opacity:1; visibility:visible; transform:translateY(0); }
.filter-option{
  display:block; width:100%; text-align:left;
  font-size:14px; color:var(--paper-text); background:none; border:none;
  padding:10px 12px; border-radius:5px; cursor:pointer;
}
.filter-option:hover{ background:var(--paper-soft); }
.filter-option.active{ color:var(--gold); font-weight:600; }
.filter-option .count{ color:var(--paper-dim); font-family:var(--font-mono); font-size:11px; margin-left:6px; }

/* Catalogue search — one box across sealed product and singles. Shares the
   pill shape and mono type of the category dropdown so the toolbar reads as
   one control cluster rather than two unrelated widgets. */
/* Basis is deliberately small: the group switch and the sub-dropdown take
   their natural width first, and the search absorbs whatever is left over.
   With a larger basis it grew past the row and pushed the result count onto
   a line of its own. */
.shop-search{
  position:relative; display:flex; align-items:center;
  flex:1 1 200px; max-width:380px;
}
.shop-search-icon{
  position:absolute; left:14px; color:var(--paper-dim); pointer-events:none;
}
.shop-search input{
  width:100%;
  font-family:var(--font-mono); font-size:12px; letter-spacing:.03em;
  padding:10px 34px 10px 36px;
  border:1px solid var(--paper-line); border-radius:20px;
  background:#fff; color:var(--paper-text);
  transition:border-color .25s, box-shadow .25s;
}
.shop-search input::placeholder{ color:var(--paper-dim); text-transform:uppercase; letter-spacing:.05em; }
.shop-search input:hover{ border-color:var(--paper-text); }
.shop-search input:focus{
  outline:none; border-color:var(--gold); box-shadow:0 0 0 3px rgba(201,162,39,.14);
}
/* Chrome adds its own clear affordance on type=search; we ship our own. */
.shop-search input::-webkit-search-decoration,
.shop-search input::-webkit-search-cancel-button{ -webkit-appearance:none; appearance:none; }
.shop-search-clear{
  position:absolute; right:8px;
  display:flex; align-items:center; justify-content:center;
  width:22px; height:22px; border:none; border-radius:50%;
  background:var(--paper-soft); color:var(--paper-dim);
  font-size:11px; cursor:pointer; transition:background .2s, color .2s;
}
.shop-search-clear:hover{ background:var(--ink); color:var(--gold-bright); }

.shop-count{
  margin-left:auto; align-self:center; flex-shrink:0;
  font-size:11px; letter-spacing:.06em; text-transform:uppercase; color:var(--paper-dim);
}

.shop-empty{
  grid-column:1 / -1; text-align:center; color:var(--paper-dim);
  padding:60px 20px; max-width:46ch; margin:0 auto;
}
.shop-more{ display:flex; justify-content:center; margin-top:44px; }

@media (max-width:640px){
  .shop-search{ flex-basis:100%; max-width:none; order:3; }
  .shop-count{ order:2; margin-left:auto; }
  /* The three group buttons plus their counts don't fit one phone row beside
     the dropdown, so the switch takes a row of its own and splits evenly. */
  .group-switch{ order:0; flex-basis:100%; }
  .group-btn{ flex:1; justify-content:center; padding:8px 6px; font-size:11px; letter-spacing:.02em; }
  .filter-dropdown{ order:1; }
}
@media (max-width:380px){
  .group-btn .count{ display:none; }
}

/* Fully fluid grid, no manual breakpoints and no view-mode toggle: a track
   is never asked to be narrower than min(45%, 260px), so at MOST 2 tracks
   ever fit below ~578px wide (2 x 45% = 90% of the row) — that's the "never
   fewer than 2 per row" floor. Above that, 260px is the per-card cap, so
   extra width just adds more columns automatically (3 around 780px+ in the
   1180px container, 4 on very wide screens) with no JS involved. */
.product-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(min(45%, 260px), 1fr));
  gap:26px;
}
@media (max-width:560px){
  /* At true phone widths two-up genuinely gets tight, so trim the card back
     to its essentials rather than shrinking everything proportionally. */
  .product-grid{ gap:14px; }
  .product-grid .product-body{ padding:12px 12px 14px; gap:5px; }
  .product-grid .product-body h3{ font-size:14px; }
  .product-grid .p-cn, .product-grid .p-meta{ display:none; }
  .product-grid .product-foot{ flex-direction:column; align-items:stretch; gap:8px; }
  .product-grid .add-btn{ text-align:center; }
}

.product-card{
  background:#fff; border:1px solid var(--paper-line); border-radius:var(--radius);
  overflow:hidden; display:flex; flex-direction:column;
  transition:box-shadow .3s, transform .3s var(--ease);
}
.product-card:hover{ box-shadow:var(--shadow-lg); transform:translateY(-4px); }
.product-media{
  aspect-ratio:4/3; position:relative; overflow:hidden;
  background:linear-gradient(150deg,var(--swatch-a,#1c1408),var(--swatch-b,#0d0f12));
  display:flex; align-items:center; justify-content:center;
}
.product-link{ color:inherit; }
.product-title-link{ color:inherit; transition:color .25s; }
.product-title-link:hover, .product-title-link:focus-visible{ color:var(--gold-dark); }
.product-media::after{
  content:""; position:absolute; inset:0;
  background:linear-gradient(120deg,transparent 30%, rgba(255,255,255,.18) 46%, transparent 60%);
  background-size:250% 250%; animation:swatchSheen 8s ease-in-out infinite;
}
.product-media .glyph{ font-family:var(--font-display); font-style:italic; font-size:30px; color:rgba(255,255,255,.8); z-index:1; }
/* Real product shots are on white, so the tile switches to a light ground
   and the sheen overlay is dropped — it only reads well over the gradients. */
.product-media.has-photo{ background:#fff; }
.product-media.has-photo::after{ display:none; }
.product-media img{
  width:100%; height:100%; object-fit:contain; padding:10px;
  transition:transform .5s var(--ease);
}
.product-card:hover .product-media img{ transform:scale(1.05); }
.product-tag{
  position:absolute; top:10px; left:10px; z-index:2;
  font-family:var(--font-mono); font-size:10px; letter-spacing:.06em; text-transform:uppercase;
  background:var(--ink); color:var(--gold-bright); padding:4px 8px; border-radius:2px;
}
.product-body{ padding:18px 18px 20px; display:flex; flex-direction:column; gap:8px; flex:1; }
.product-body .p-cat{ font-family:var(--font-mono); font-size:10px; letter-spacing:.08em; text-transform:uppercase; color:var(--paper-dim); }
.product-body h3{ font-size:19px; }
.product-body .p-cn{ font-family:var(--font-mono); font-size:12px; color:var(--paper-dim); }
.product-body .p-meta{ font-size:13px; color:var(--paper-dim); margin:2px 0 8px; }
.product-foot{ margin-top:auto; display:flex; align-items:center; justify-content:space-between; gap:10px; }
.product-foot .p-price{ font-family:var(--font-mono); font-size:13px; color:var(--paper-text); }
.product-foot .p-price small{ display:block; color:var(--paper-dim); font-size:10px; text-transform:uppercase; letter-spacing:.05em; }
.add-btn{
  font-family:var(--font-mono); font-size:12px; text-transform:uppercase; letter-spacing:.06em;
  background:var(--ink); color:var(--ink-text); border:none; padding:10px 14px; border-radius:2px;
  transition:background .25s;
}
.add-btn:hover{ background:var(--gold); color:#171204; }
.add-btn.added{ background:var(--gold); color:#171204; }

/* Single cards ------------------------------------------------------------
   A single is a portrait card, not a boxy sealed product, so its tile is
   taller and keeps the dark gradient + sheen instead of the white ground used
   for sealed photography. `.is-card` therefore has to undo `.has-photo`
   (which the sync script also sets, since singles do carry an image): the card
   art is a full-bleed rectangle that reads far better floating on ink with a
   little light moving across it — the same foil language as the hero card. */
.product-media.is-card{ aspect-ratio:3/4; }
.product-media.has-photo.is-card{
  background:linear-gradient(150deg,var(--swatch-a,#1c1408),var(--swatch-b,#0d0f12));
}
.product-media.has-photo.is-card::after{ display:block; }
.product-media.is-card img{
  padding:16px;
  filter:drop-shadow(0 10px 18px rgba(0,0,0,.45));
}
/* The sheen sits above the artwork, so it must not swallow the click. */
.product-media.is-card::after{ pointer-events:none; }

@media (max-width:560px){
  .product-media.is-card img{ padding:9px; }
}

/* ==========================================================================
   Closing CTA card (homepage) — everything centred inside one white card.
   Padding, type size and measure all scale with the viewport so the card
   never gets cramped as the screen narrows.
   ========================================================================== */
/* The closing CTA sits on a positioned section so the mascot can be pinned
   to the bottom-right of the container without leaving the flow of the page
   or adding height to it. */
.closing-section{ position:relative; }
.closing-mascot{
  position:absolute;
  /* Flush on the band's bottom edge. The sticker is cropped at the chest in
     the source art, so the only way it does not read as half a Pikachu is
     for that crop to coincide exactly with the edge of the section — then it
     is simply standing behind it. bottom:0, and nothing between. */
  bottom:0;
  /* Lines up with the right edge of the container at every width without
     needing the container as its parent. */
  right:max(28px, calc((100% - var(--container)) / 2));
  width:clamp(170px, 19vw, 260px);
  height:auto;
  z-index:2;
  pointer-events:none;
  user-select:none;
  /* Grounded rather than floating: a short, tight shadow at the feet. */
  filter:drop-shadow(0 6px 10px rgba(0,0,0,.16));
}
@media (max-width:1080px){
  /* Below this the card fills the container, so the mascot steps out to the
     very edge of the screen and gives the copy its room back. */
  .closing-mascot{ width:clamp(150px, 17vw, 190px); right:8px; }
}
@media (max-width:760px){
  /* Phones: the card runs the full width, so there is no room *beside* it —
     the band gets a taller foot instead and the mascot stands in that strip,
     still flush on the bottom edge.
     The foot is derived from the mascot's own width rather than hard-coded:
     the sprite is 412x420, so its rendered height is width x 1.02, and the
     fixed 132px foot it used to have was shorter than that. The overflow
     went upwards, which put the mascot on top of the corner of the CTA card
     instead of beside it the way the desktop layout has it. Tying the two
     together keeps it clear of the card at every phone width — and it stays
     correct if the sprite is ever recut at a different aspect. */
  .closing-section{
    --mascot-w:clamp(120px, 36vw, 152px);
    padding-bottom:calc(var(--mascot-w) * 1.02 + 20px);
  }
  .closing-mascot{
    display:block;
    width:var(--mascot-w);
    /* Clear of the quote FAB's column rather than under it. The FAB is fixed
       in the bottom-right corner, and the bottom of this band is exactly
       where the two meet — at right:12px the button sat on Pikachu's face.
       Derived from the FAB's own geometry (18px inset + 52px button) so the
       two stay apart if either is resized. */
    right:calc(18px + 52px + 10px);
  }
}
@media (max-width:400px){
  /* Narrowest phones: smaller again, and the foot follows it down. */
  .closing-section{ --mascot-w:clamp(106px, 32vw, 128px); }
  .closing-mascot{ right:calc(18px + 52px + 4px); }
}

.closing-cta{
  background:#fff;
  border:1px solid var(--paper-line);
  border-radius:6px;
  padding:clamp(30px,4.4vw,54px) clamp(20px,4vw,52px);
  text-align:center;
}
.closing-cta h2{
  font-size:clamp(26px,3.2vw,40px);
  /* Wide measure so the line breaks once, not twice — 24ch forced it onto
     three lines. `min(100%, …)` keeps it inside the padded card on narrow
     screens instead of overflowing. */
  max-width:min(100%, 30ch);
  margin:0 auto 18px;
  text-wrap:balance;          /* even line lengths where supported */
}
/* Second sentence of the margin statement — italic so the two halves read
   as a pair ("our margin" / "your margin"). The .shine variant keeps the
   gold sweep and must not have its clipped-text colour overridden here. */
.closing-cta h2 i{ font-style:italic; }
.closing-cta h2 i:not(.shine){ color:var(--gold); }
.closing-cta p{
  max-width:min(100%, 54ch);
  margin:0 auto 26px;
  color:var(--paper-dim);
}
/* Same grid as the hero actions: both buttons take the width of the widest
   label rather than each sizing to its own text. The card is text-align
   centred, so the inline-grid centres itself. */
.closing-cta-actions{
  display:inline-grid;
  grid-auto-flow:column;
  /* minmax(0,1fr), not 1fr: a bare 1fr floors each column at the button's
     min-content width, so on a narrow screen the pair kept its full size and
     the second button ("Ga naar catalogus") hung outside the card. */
  grid-auto-columns:minmax(0,1fr);
  /* Shrink-wrap the pair, but never past the card. */
  width:min(100%, max-content);
  gap:14px;
}
@media (max-width:560px){
  /* Side-by-side buttons get too tight to read below this — stack them
     rather than letting the labels squeeze. */
  .closing-cta-actions{ grid-auto-flow:row; width:100%; }
}

/* ==========================================================================
   Feature cards — three editorial doorways, revealed by a running Pokémon
   ==========================================================================
   Layer order inside a card (all siblings of the <a>, so nothing nests
   another interactive element):

     1  .fc-media   the photo, clipped away at rest and wiped open by JS
     2  .fc-veil    gradient that buys the copy its contrast
     3  .fc-mon…    the character — deliberately ABOVE the veil, so it reads
                    as running in front of the picture rather than inside it
     4  .fc-body    index / title / description
     5  .fc-cta     arrow, inside .fc-body

   Everything here is visible-by-default: the pre-reveal state (photo clipped,
   copy hidden, character parked off the card) only exists under .fc-ready,
   which JS adds once it knows it can animate. No JS, no GSAP or reduced
   motion → the cards render finished, and only the hover polish is lost.
   ========================================================================== */
.feature{
  background:var(--ink);
  color:var(--ink-text);
  padding:clamp(84px,9vw,120px) 0;
}
.feature-head{ margin-bottom:clamp(30px,4vw,52px); }
.feature-head h2{ font-size:clamp(26px,3.2vw,40px); max-width:20ch; }

/* Editorial, not a three-up grid: the catalogue card owns the left column at
   full height, the other two stack beside it, and the right column starts a
   step lower so the top edge never runs straight across. */
.feature-grid{
  display:grid;
  grid-template-columns:1.16fr 1fr;
  gap:clamp(14px,1.7vw,24px);
  align-items:stretch;
}
.feature-card--lg{ grid-row:1 / span 2; min-height:clamp(430px,45vw,600px); }
.feature-card--sm{ min-height:clamp(206px,21vw,288px); }
/* The two columns run to exactly the same height: 600 on the left, and
   288 + 24 gap + 288 on the right. The asymmetry in this composition comes
   from the column split itself — every card's copy starts at the same left
   edge, so the three read as one set rather than as a mirrored pair. */

.feature-card{
  position:relative;
  display:flex;
  flex-direction:column;
  justify-content:flex-end;
  padding:clamp(20px,2.3vw,34px);
  border:1px solid var(--ink-line);
  border-radius:6px;
  background:var(--ink-soft);
  color:var(--ink-text);
  isolation:isolate;
  /* The character is allowed to hang past the card edge — only the photo is
     clipped, by .fc-media below. */
  overflow:visible;
  transform:perspective(1400px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)) translate3d(0,var(--ty,0px),0);
  transform-style:preserve-3d;
  transition:transform .55s var(--ease), border-color .45s var(--ease),
             box-shadow .55s var(--ease), opacity .6s var(--ease);
}
.feature-card:hover{
  --ty:-5px;
  border-color:rgba(201,162,39,.42);
  box-shadow:0 34px 64px -34px rgba(0,0,0,.9);
}

/* ---------- 1. photo ---------- */
.fc-media{
  position:absolute; inset:0;
  z-index:1;
  border-radius:inherit;
  overflow:hidden;
  /* What sits behind the un-wiped part: a near-black plate with the faintest
     ledger rule, so a card waiting to be revealed still reads as a surface
     rather than a hole. */
  background:
    repeating-linear-gradient(115deg, rgba(201,162,39,.05) 0 1px, transparent 1px 26px),
    radial-gradient(120% 90% at 18% 8%, #171b22 0%, #0b0d11 62%);
}
.fc-img{
  width:100%; height:100%;
  object-fit:cover;
  /* Slight overscale at rest so the hover zoom has somewhere to go and never
     exposes an edge. */
  transform:scale(1.03);
  transition:transform .9s var(--ease);
}
.feature-card:hover .fc-img{ transform:scale(1.07); }

/* ---------- 2. veil ---------- */
.fc-veil{
  position:absolute; inset:0;
  z-index:2;
  border-radius:inherit;
  background:
    linear-gradient(to top, rgba(8,9,12,.95) 0%, rgba(8,9,12,.84) 24%, rgba(8,9,12,.44) 50%, rgba(8,9,12,.08) 78%),
    linear-gradient(to right, rgba(8,9,12,.60), rgba(8,9,12,0) 62%),
    /* A flat sit-down over the whole photo. The three shots are studio-lit on
       white and would otherwise read as three different brightnesses against
       the ink; this pulls them onto one level. */
    linear-gradient(rgba(8,9,12,.22), rgba(8,9,12,.22));
  transition:opacity .5s var(--ease);
}
.feature-card:hover .fc-veil{ opacity:.88; }

/* ---------- 3. character ---------- */
/* .fc-mon-track spans the whole card, so GSAP's xPercent/yPercent are
   percentages of the CARD, not of the sprite — the choreography stays the
   same at every breakpoint with no pixel maths anywhere.
   .fc-mon-anchor parks the sprite just off the left edge (right:100%), which
   is the zero point every travel value in FEATURE_MOTION is measured from,
   and carries the pointer parallax so it never fights the run.
   .fc-mon-bob carries the run cycle. Four elements, four jobs, no shared
   transform between them. */
/* The clip has to sit on a element that never moves: clip-path is resolved in
   the element's own coordinate space, so putting it on the track would drag
   the clip along with the character and clip nothing. Three sides are cut at
   the card edge, so a character can never end up stranded in the page gutter;
   the top is left open by 40px so a tall sprite or a leap still breaks the
   frame, which is what keeps the characters reading as a layer in front of
   the composition rather than something living inside it. */
.fc-mon-clip{
  position:absolute; inset:0;
  z-index:3;
  pointer-events:none;
  clip-path:inset(-40px 0 0 0 round 6px);
}
.fc-mon-track{ position:absolute; inset:0; }
.fc-mon-anchor{
  position:absolute;
  right:100%;
  bottom:var(--mon-bottom,10%);
  transform:translate3d(var(--mx,0px), var(--my,0px), 0);
  transition:transform .6s var(--ease);
}
.fc-mon-bob{ transform-origin:50% 100%; }
.fc-mon{
  height:var(--mon-h,140px);
  width:auto;
  max-width:none;
  transform:scaleX(var(--mon-flip,1));
  filter:drop-shadow(0 14px 18px rgba(0,0,0,.45));
}

/* ---------- 4/5. copy ---------- */
.fc-body{
  position:relative;
  z-index:4;
  max-width:34ch;
}
.fc-title{
  font-size:clamp(22px,2.4vw,34px);
  margin-bottom:10px;
  text-wrap:balance;
}
.feature-card--lg .fc-title{ font-size:clamp(26px,3vw,42px); }
.fc-desc{
  margin:0 0 18px;
  font-size:14px;
  line-height:1.5;
  color:rgba(236,234,227,.74);
  max-width:30ch;
}
.fc-cta{
  position:relative;
  z-index:5;
  display:inline-flex; align-items:center; gap:10px;
  font-family:var(--font-mono);
  font-size:11px; letter-spacing:.14em; text-transform:uppercase;
  color:var(--ink-text);
  padding-bottom:4px;
  border-bottom:1px solid rgba(201,162,39,.45);
  transition:color .35s var(--ease), border-color .35s var(--ease);
}
.fc-cta svg{
  width:17px; height:17px; flex:0 0 auto;
  fill:none; stroke:currentColor; stroke-width:1.6;
  stroke-linecap:round; stroke-linejoin:round;
  transition:transform .4s var(--ease);
}
.feature-card:hover .fc-cta{ color:var(--gold-bright); border-color:var(--gold-bright); }
.feature-card:hover .fc-cta svg{ transform:translateX(5px); }
.feature-card:focus-visible{ outline-offset:5px; }
.feature-card:focus-visible .fc-cta svg{ transform:translateX(5px); }

/* ---------- pre-reveal state (JS-gated) ---------- */
/* Only applied once .fc-ready is on the grid, i.e. once something is going to
   animate it back. The photo's own clip-path is set inline by JS from the
   same value that drives the character, so it is not repeated here. */
.fc-ready .feature-card{ --ty:26px; opacity:0; }
.fc-ready .feature-card.is-in{ --ty:0px; opacity:1; }
.fc-ready .fc-body > *{
  opacity:0;
  transform:translateY(14px);
  transition:opacity .6s var(--ease), transform .6s var(--ease);
}
.fc-ready .is-in .fc-body > *{ opacity:1; transform:none; }
/* Copy lands after the sprite has crossed, arrow last. */
.fc-ready .is-in .fc-body > *:nth-child(1){ transition-delay:.42s; }
.fc-ready .is-in .fc-body > *:nth-child(2){ transition-delay:.50s; }
.fc-ready .is-in .fc-body > *:nth-child(3){ transition-delay:.58s; }
.fc-ready .is-in .fc-body > *:nth-child(4){ transition-delay:.70s; }

/* ---------- responsive ---------- */
@media (max-width:1024px){
  /* Tablet: one column, and the tall card gives up some height — the reveal
     still plays, it just has less distance to cover. */
  .feature-grid{ grid-template-columns:1fr; }
  .feature-card--lg{ grid-row:auto; min-height:clamp(330px,44vw,420px); }
  .feature-card--sm{ min-height:clamp(240px,32vw,300px); }
}

.feature-dots{ display:none; }

/* --------------------------------------------------------------------------
   Phones: one swipeable row instead of a stack.
   Three full-height cards stacked cost about 1150px of scrolling for what is
   essentially a three-item menu. Laid on one snapping row they cost ~380px,
   and each card keeps the full width of the screen — which the reveal needs,
   since the character has to have somewhere to run.

   Scroll-snap, not a JS carousel: the browser's own scrolling is smoother
   than anything scripted, works with a flick, and needs no state. The next
   card is deliberately left peeking past the right edge — that sliver is what
   tells a first-time visitor there is more here without a hint or an arrow.
   -------------------------------------------------------------------------- */
@media (max-width:640px){
  .feature-grid{
    display:flex;
    gap:12px;
    /* Full-bleed: the row scrolls edge to edge while the copy above it stays
       inside the container, which is what makes the peeking card read as "the
       row continues" rather than "the layout is broken". */
    margin-inline:calc(50% - 50vw);
    padding-inline:20px;
    overflow-x:auto;
    overflow-y:hidden;
    scroll-snap-type:x mandatory;
    scroll-padding-inline:20px;
    -webkit-overflow-scrolling:touch;
    scrollbar-width:none;
  }
  .feature-grid::-webkit-scrollbar{ display:none; }
  .feature-card{
    flex:0 0 86%;
    scroll-snap-align:start;
    /* A hidden card must not be reachable by Tab before its neighbours: the
       row is in DOM order, so this is already true — but the scroll container
       has to be told to bring a focused card into view. */
    min-height:0;
    padding:22px 20px 24px;
  }
  .feature-card--lg, .feature-card--sm{ min-height:370px; }
  /* No entrance offset on a horizontal row: a card sliding up as it snaps in
     fights the scroll direction. */
  .fc-ready .feature-card{ --ty:0px; }
  .fc-body{ max-width:none; }
  .fc-desc{ max-width:34ch; }
  .feature-dots{
    display:flex; justify-content:center; gap:7px;
    margin-top:20px;
  }
  .feature-dots span{
    width:6px; height:6px; border-radius:50%;
    background:var(--ink-line);
    transition:background .3s var(--ease), width .3s var(--ease);
  }
  .feature-dots span.on{ width:20px; border-radius:3px; background:var(--gold); }
  /* Sprites shrink on phones so they stay a foreground detail rather than
     covering the copy they run past, and they run across the upper half of
     the card instead of along its floor: with one column the copy is full
     width down there, and a character resting on top of it is the one thing
     this section must never do. (!important because the height is an inline
     per-card variable — this is the breakpoint overriding the instance.) */
  .feature-card{ --mon-h:clamp(78px,22vw,112px) !important; }
  .feature-card--lg, .feature-card--sm{ --mon-bottom:51% !important; }
  .fc-mon{ filter:drop-shadow(0 8px 12px rgba(0,0,0,.45)); }
}

@media (prefers-reduced-motion: reduce){
  /* Nothing moves and nothing is hidden: no run, no wipe, no lift, no zoom,
     no 3D. The section is fully functional, just still. */
  .feature-card,
  .fc-img, .fc-veil, .fc-cta, .fc-cta svg, .fc-mon-anchor,
  .fc-ready .fc-body > *{ transition:none !important; }
  .feature-card{ --ty:0px !important; --rx:0deg !important; --ry:0deg !important; opacity:1 !important; }
  .fc-ready .fc-body > *{ opacity:1 !important; transform:none !important; }
  .feature-card:hover .fc-img{ transform:scale(1.03); }
}

/* ==========================================================================
   Forms — customs/ledger style
   ========================================================================== */
.form-shell{
  background:#fff; border:1px solid var(--paper-line); border-radius:6px;
  padding:44px; box-shadow:var(--shadow-lg);
}
@media (max-width:640px){ .form-shell{ padding:28px 22px; } }
.form-shell .form-title{ font-size:15px; }
.form-legend{
  font-family:var(--font-mono); font-size:11px; letter-spacing:.1em; text-transform:uppercase;
  color:var(--gold); display:block; margin:30px 0 16px; padding-top:20px; border-top:1px solid var(--paper-line);
}
/* .is-first is set from JS when the block above a legend has been hidden —
   see initQuoteIdentity(). */
.form-legend:first-child, .form-legend.is-first{ margin-top:0; padding-top:0; border-top:none; }
.form-row{ display:grid; grid-template-columns:1fr 1fr; gap:18px; margin-bottom:18px; }
.form-row.single{ grid-template-columns:1fr; }
@media (max-width:600px){ .form-row{ grid-template-columns:1fr; } }
.field label{
  display:block; font-family:var(--font-mono); font-size:11px; text-transform:uppercase;
  letter-spacing:.06em; color:var(--paper-dim); margin-bottom:7px;
}
.field .hint{ font-size:12px; color:var(--paper-dim); margin-top:5px; }
.field input, .field select, .field textarea{
  width:100%; border:1px solid var(--paper-line); border-radius:3px;
  padding:12px 13px; background:var(--paper); color:var(--paper-text);
  transition:border-color .25s;
}
.field input:focus, .field select:focus, .field textarea:focus{ border-color:var(--paper-text); outline:none; }
.field textarea{ resize:vertical; min-height:96px; }
.checkbox-row{ display:flex; gap:10px; align-items:flex-start; font-size:13px; color:var(--paper-dim); margin:22px 0; }
.checkbox-row input{ margin-top:3px; }
.form-note{
  font-size:12px; color:var(--paper-dim); background:var(--paper-soft); border-radius:3px;
  padding:14px 16px; margin-bottom:26px; border:1px dashed var(--paper-line);
}
.form-success{ text-align:center; padding:40px 10px; display:none; }
.form-success.show{ display:block; }

/* two-step account wizard */
.form-narrow{ max-width:580px; margin:0 auto; }
.wizard-steps{ display:flex; align-items:center; gap:14px; margin-bottom:32px; }
.wstep{ display:flex; align-items:center; gap:10px; font-size:14px; color:var(--paper-dim); white-space:nowrap; }
.wstep .num{
  width:28px; height:28px; border-radius:50%;
  background:var(--paper-soft); color:var(--paper-dim);
  display:flex; align-items:center; justify-content:center;
  font-family:var(--font-mono); font-size:13px;
  transition:background .3s, color .3s;
}
.wstep.active{ color:var(--paper-text); font-weight:600; }
.wstep.active .num{ background:var(--ink); color:var(--gold); }
.wizard-line{ flex:1; height:1px; background:var(--paper-line); }
.wpanel{ display:none; }
.wpanel.active{ display:block; }
.wizard-back{
  display:block; width:100%; margin-top:14px; padding:10px;
  background:none; border:none; color:var(--paper-dim);
  font-family:var(--font-mono); font-size:12px; letter-spacing:.06em; text-transform:uppercase;
}
.wizard-back:hover{ color:var(--paper-text); }
.form-success .stamp{
  display:inline-flex; align-items:center; justify-content:center;
  border:2px solid var(--red-stamp); color:var(--red-stamp);
  font-family:var(--font-mono); font-size:13px; letter-spacing:.1em; text-transform:uppercase;
  padding:10px 22px; border-radius:4px; transform:rotate(-4deg); margin-bottom:20px;
}

/* ==========================================================================
   Table (price list) — mono ledger
   ========================================================================== */
.ledger{ width:100%; border-collapse:collapse; font-size:14px; }
.ledger th{
  text-align:left; font-family:var(--font-mono); font-size:11px; text-transform:uppercase;
  letter-spacing:.08em; color:var(--paper-dim); padding:12px 14px; border-bottom:1px solid var(--paper-text);
}
.ledger td{ padding:16px 14px; border-bottom:1px solid var(--paper-line); vertical-align:top; }
.ledger tr:hover td{ background:var(--paper-soft); }
.ledger .num{ font-family:var(--font-mono); }
.ledger-wrap{ overflow-x:auto; border:1px solid var(--paper-line); border-radius:4px; }
.ledger-wrap .ledger{ min-width:720px; }
.price-product-link{ color:var(--paper-text); text-decoration:underline; text-decoration-color:rgba(184,137,43,.45); text-underline-offset:4px; transition:color .2s, text-decoration-color .2s; }
.price-product-link:hover, .price-product-link:focus-visible{ color:var(--gold-dark); text-decoration-color:currentColor; }

/* Product detail — the same catalogue information, given a calmer product-led canvas. */
.product-detail{ padding:54px 0 88px; min-height:calc(100vh - 76px); }
.product-back{ display:inline-flex; margin-bottom:30px; color:var(--paper-dim); font-family:var(--font-mono); font-size:11px; letter-spacing:.07em; text-transform:uppercase; }
.product-back:hover{ color:var(--gold-dark); }
.product-detail-grid{ display:grid; grid-template-columns:minmax(0,1.08fr) minmax(300px,.92fr); gap:clamp(34px,6vw,88px); align-items:start; }
.product-detail-media{ position:relative; aspect-ratio:1/1; border:1px solid var(--paper-line); border-radius:var(--radius); overflow:hidden; display:flex; align-items:center; justify-content:center; background:linear-gradient(150deg,var(--swatch-a,#1c1408),var(--swatch-b,#0d0f12)); box-shadow:var(--shadow-lg); }
.product-detail-media.has-photo{ background:#fff; }
/* A single keeps the ink ground rather than the white photo ground, so the
   detail hero matches the card tiles in the grid it was clicked from. */
.product-detail-media.has-photo.is-card{
  background:linear-gradient(150deg,var(--swatch-a,#1c1408),var(--swatch-b,#0d0f12));
}
.product-detail-media.is-card img{ filter:drop-shadow(0 14px 26px rgba(0,0,0,.5)); }
.product-detail-media img{ display:block; width:100%; height:100%; object-fit:contain; padding:28px; }
.product-detail-media .glyph{ font-family:var(--font-display); font-size:clamp(44px,7vw,84px); color:rgba(255,255,255,.85); font-style:italic; text-align:center; padding:28px; }
.product-detail-copy{ display:flex; flex-direction:column; min-height:100%; }
.product-detail-copy h1{ margin:8px 0; font-size:clamp(38px,4.2vw,58px); line-height:.98; }
.product-detail-copy .p-cat, .product-detail-copy .p-cn{ font-family:var(--font-mono); font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:var(--paper-dim); }
.product-detail-copy .p-cn{ display:block; margin-top:8px; }
.product-detail-set{ color:var(--paper-dim); font-size:16px; line-height:1.6; margin:28px 0 0; max-width:42ch; }
.variant-switch{
  display:flex; gap:10px; margin:20px 0 28px; flex-wrap:wrap;
}
.variant-option{
  background:none; border:1px solid var(--paper-line); color:var(--paper-dim);
  font-family:var(--font-mono); font-size:11px; text-transform:uppercase; letter-spacing:.06em;
  padding:10px 22px; border-radius:999px; transition:color .2s, border-color .2s, background .2s;
}
/* The design switch can hold nine options, so it sits tighter than the
   two-option format switch above it. */
.design-switch{ gap:8px; margin:20px 0 12px; }
.design-switch .variant-option{ padding:8px 14px; }
.variant-option:hover{ color:var(--paper-text); border-color:var(--paper-text); }
.variant-option.active{ color:var(--gold); border-color:var(--gold); background:rgba(201,162,39,.1); }
.product-specs{ border-top:1px solid var(--paper-line); margin:0 0 30px; }
.product-specs div{ display:flex; justify-content:space-between; gap:20px; padding:14px 0; border-bottom:1px solid var(--paper-line); }
.product-specs dt{ color:var(--paper-dim); font-size:13px; }
.product-specs dd{ margin:0; font-family:var(--font-mono); font-size:12px; text-align:right; }
.product-detail-add.added{ background:var(--ink); color:var(--gold-bright); }
.product-not-found{ text-align:center; padding:84px 0; }
.product-not-found p{ color:var(--paper-dim); margin:14px auto 24px; }
.product-related{ margin-top:88px; padding-top:48px; border-top:1px solid var(--paper-line); }
.product-related h2{ font-size:clamp(22px,2.6vw,30px); margin-bottom:24px; }
@media (max-width:760px){
  .product-detail{ padding:34px 0 60px; }
  .product-detail-grid{ grid-template-columns:1fr; gap:30px; }
  .product-detail-media{ aspect-ratio:4/3; }
  .product-detail-media img{ padding:18px; }
  .product-related{ margin-top:56px; padding-top:34px; }
}

/* ==========================================================================
   Contact page — two-state box (form <-> WhatsApp), slides horizontally
   ========================================================================== */
.contact-box{
  position:relative;
  overflow:hidden;
}
.contact-box-track{
  display:flex;
  width:200%;
  transition:transform .55s var(--ease);
}
.contact-box.wa-active .contact-box-track{ transform:translateX(-50%); }
.contact-pane{
  position:relative;
  width:50%;
  flex-shrink:0;
  padding:44px;
  min-width:0;
}
@media (max-width:640px){ .contact-pane{ padding:28px 22px; } }

.contact-toggle{
  display:flex; align-items:center; justify-content:center; gap:16px;
  width:100%; margin-top:30px; padding:16px 10px 6px;
  border:none; border-top:1px solid var(--paper-line); background:none;
  cursor:pointer; text-align:center;
}
.contact-toggle-text{ display:flex; flex-direction:column; gap:2px; }
.contact-toggle-label{
  font-family:var(--font-display); font-size:18px; color:var(--paper-text);
}
.contact-toggle-sub{
  font-family:var(--font-mono); font-size:12px; letter-spacing:.04em;
  color:var(--paper-dim);
}
.contact-arrow{
  position:relative; flex-shrink:0; overflow:hidden;
  display:inline-flex; align-items:center; justify-content:center;
  width:38px; height:38px; border-radius:50%;
  border:1px solid var(--paper-line); background:#fff; color:var(--paper-dim);
  transition:border-color .25s, color .25s, transform .25s var(--ease);
}
.contact-toggle:hover .contact-arrow, .contact-toggle:focus-visible .contact-arrow{
  border-color:var(--gold); color:var(--gold-dark,var(--gold)); transform:translateX(3px);
}
.contact-toggle.back:hover .contact-arrow, .contact-toggle.back:focus-visible .contact-arrow{
  transform:translateX(-3px);
}
.contact-arrow::after{
  content:""; position:absolute; inset:0; border-radius:50%;
  background:linear-gradient(115deg, transparent 30%, rgba(191,161,90,.55) 50%, transparent 70%);
  background-size:220% 100%; background-position:120% 0;
  animation:contact-arrow-shine 3.2s ease-in-out infinite;
  animation-delay:1.4s;
}
@keyframes contact-arrow-shine{
  0%{ background-position:130% 0; opacity:0; }
  35%{ opacity:1; }
  60%{ background-position:-30% 0; opacity:0; }
  100%{ background-position:-30% 0; opacity:0; }
}
@media (max-width:640px){
  .contact-toggle{ gap:12px; padding:14px 6px 4px; }
  .contact-arrow{ width:34px; height:34px; }
}

.contact-swipe-hint{
  display:none;
  text-align:center; margin-top:14px;
  font-family:var(--font-mono); font-size:11px; letter-spacing:.05em;
  color:var(--paper-dim); opacity:.75;
}
@media (max-width:640px){
  .contact-swipe-hint{ display:block; }
}

.wa-qr-wrap{
  display:flex; flex-direction:column; align-items:center; text-align:center;
  padding:46px 0 4px;
}
.wa-qr-frame{
  width:min(220px,60vw); aspect-ratio:1/1; padding:14px;
  background:#fff; border:1px solid var(--paper-line); border-radius:10px;
  box-shadow:0 18px 40px -22px rgba(0,0,0,.35);
  margin-bottom:24px;
}
.wa-qr-frame img, .wa-qr-frame svg{ width:100%; height:100%; display:block; }
.wa-qr-wrap h3{ font-size:22px; margin-bottom:10px; }
.wa-qr-wrap p{ color:var(--paper-dim); max-width:38ch; margin:0 auto; }

/* ==========================================================================
   FAQ accordion
   ========================================================================== */
.faq-item{ border-bottom:1px solid var(--paper-line); }
.faq-q{
  width:100%; text-align:left; background:none; border:none; padding:24px 0;
  display:flex; justify-content:space-between; align-items:center; gap:20px;
  font-family:var(--font-display); font-size:19px; color:var(--paper-text);
}
.faq-q .plus{ font-family:var(--font-mono); font-size:20px; color:var(--gold); transition:transform .3s; flex-shrink:0; }
.faq-item.open .plus{ transform:rotate(45deg); }
.faq-a{ max-height:0; overflow:hidden; transition:max-height .4s var(--ease); }
.faq-item.open .faq-a{ max-height:340px; }
.faq-a p{ padding:0 0 26px; color:var(--paper-dim); max-width:66ch; }

/* ==========================================================================
   Quote/cart page
   ========================================================================== */
.cart-table{ width:100%; border-collapse:collapse; margin-bottom:10px; }
.cart-table th{ text-align:left; font-family:var(--font-mono); font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--paper-dim); padding:10px 12px; border-bottom:1px solid var(--paper-text); }
.cart-table td{ padding:16px 12px; border-bottom:1px solid var(--paper-line); }
.cart-item{ display:flex; align-items:center; gap:14px; }
.cart-item .swatch{
  width:52px; height:52px; border-radius:3px; flex-shrink:0; overflow:hidden;
  background:linear-gradient(150deg,var(--swatch-a,#1c1408),var(--swatch-b,#0d0f12));
}
.cart-item .swatch:has(img){ background:#fff; border:1px solid var(--paper-line); }
.cart-item .swatch img{ width:100%; height:100%; object-fit:contain; padding:3px; }
.cart-item .name{ font-size:14px; }
.cart-item .cat{ font-family:var(--font-mono); font-size:11px; color:var(--paper-dim); }
.qty-input{ width:60px; padding:8px; border:1px solid var(--paper-line); border-radius:3px; font-family:var(--font-mono); text-align:center; }
.remove-btn{ font-family:var(--font-mono); font-size:11px; text-transform:uppercase; color:var(--paper-dim); background:none; border:none; }
.remove-btn:hover{ color:var(--red-stamp); }
@media (max-width:640px){
  /* MOQ is secondary info (it's shown per-product in the shop already) —
     drop that column so item name + qty + remove all fit on a phone. */
  .cart-table .col-moq{ display:none; }
  .cart-table td, .cart-table th{ padding:14px 6px; }
  .cart-item{ gap:10px; }
  .cart-item .name{ font-size:13px; }
  .qty-input{ width:44px; padding:6px 4px; }
  .remove-btn{ font-size:15px; padding:6px; }
}
.cart-empty{ text-align:center; padding:70px 20px; }
.cart-empty p{ color:var(--paper-dim); margin:14px 0 26px; }
.gate-note{
  border:1px dashed var(--gold); background:rgba(201,162,39,.07); border-radius:4px;
  padding:22px 24px; margin-bottom:34px; display:flex; gap:16px; align-items:flex-start;
}
.gate-note .g-ico{ font-family:var(--font-mono); color:var(--gold); font-size:20px; }
.gate-note h4{ font-family:var(--font-body); font-weight:700; font-size:14px; margin:0 0 6px; }
.gate-note p{ margin:0; font-size:13px; color:var(--paper-dim); }
.gate-note a{ color:var(--gold); text-decoration:underline; }

/* ==========================================================================
   Customer dashboard
   ==========================================================================
   Built almost entirely out of what the rest of the site already has: the
   quote lines reuse .ledger-wrap/.ledger from the price list, the notices
   reuse .gate-note from the quote page, the empty state reuses .cart-empty.
   What is genuinely new is the status pill, the quote list, and the
   confirmation dialog in front of accepting an offer.
   ========================================================================== */
.dash-loading{ color:var(--paper-dim); font-family:var(--font-mono); font-size:13px; padding:40px 0; }
.dash-h2{ font-size:26px; margin-bottom:22px; }

/* A quote list is two facts and a state per row, and on a phone those want to
   stack rather than scroll sideways — which is why this is not a table like
   the lines inside it are. */
.q-list{ display:flex; flex-direction:column; gap:10px; }
.q-card{
  display:flex; align-items:center; justify-content:space-between; gap:20px;
  padding:18px 20px; background:#fff;
  border:1px solid var(--paper-line); border-radius:4px;
  transition:border-color .25s, transform .25s var(--ease);
}
.q-card:hover{ border-color:var(--paper-text); transform:translateY(-1px); }
/* The one row the customer has to do something about earns a gold edge. */
.q-card.needs-action{ border-left:3px solid var(--gold); }
.q-card-main{ display:flex; flex-direction:column; gap:3px; min-width:0; }
.q-card-side{ display:flex; align-items:center; gap:18px; flex-shrink:0; }
.q-number{ font-size:14px; color:var(--paper-text); }
.q-date{ font-size:12px; color:var(--paper-dim); }
.q-total{ font-size:13px; color:var(--paper-text); }

.q-status{
  display:inline-flex; align-items:center;
  font-family:var(--font-mono); font-size:10px; letter-spacing:.09em; text-transform:uppercase;
  padding:5px 11px; border-radius:999px; border:1px solid var(--paper-line); color:var(--paper-dim);
  white-space:nowrap;
}
.q-status.is-in_behandeling{ border-color:rgba(201,162,39,.5); color:var(--gold); }
/* Quoted is the state that wants a decision, so it is the only filled one. */
.q-status.is-geoffreerd{ background:var(--gold); border-color:var(--gold); color:#171204; }
.q-status.is-geaccepteerd{ border-color:rgba(47,217,196,.55); color:#149c8b; }
.q-status.is-geannuleerd{ border-color:rgba(180,59,47,.45); color:var(--red-stamp); }

@media (max-width:640px){
  .q-card{ flex-direction:column; align-items:flex-start; gap:12px; padding:16px; }
  .q-card-side{ width:100%; justify-content:space-between; }
}

/* --- detail ---
   The header used to be a stack: heading, then four full-width rows of
   label/value, then two notes each behind a bordered legend. That pushed the
   actual quote most of a screen down, which is backwards — the lines are what
   the customer opened the page for. Everything above them is now one strip
   that wraps, so it costs about a third of the height it did. */
/* Looking at one quote: the hero shrinks to a title bar and gives its height
   to the thing being looked at. Same page, same markup — see renderDashboard(). */
body.dash-detail .page-hero p{ display:none; }
.dash-section{ padding-top:56px; }
body.dash-detail .dash-section{ padding-top:34px; }

.q-detail-back{ margin-bottom:16px; }

.q-head{ display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:16px; }
.q-head h1{ font-family:var(--font-mono); font-size:clamp(20px,2.6vw,26px); letter-spacing:.02em; }

/* Label over value, in as many columns as fit — three of these across is one
   row where it used to be three. */
.q-meta{
  display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr));
  gap:12px 26px; margin:0 0 18px; padding:13px 0;
  border-top:1px solid var(--paper-line); border-bottom:1px solid var(--paper-line);
}
.q-meta > div{ min-width:0; }
.q-meta dt{
  font-family:var(--font-mono); font-size:10px; letter-spacing:.08em;
  text-transform:uppercase; color:var(--paper-dim); margin-bottom:3px;
}
.q-meta dd{ margin:0; font-size:14px; }
.q-expired{ font-family:var(--font-mono); font-size:10px; text-transform:uppercase; color:var(--red-stamp); }

.q-notes{
  display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
  gap:14px 26px; margin-bottom:22px;
}
.q-note{ min-width:0; }
.q-note p{ margin:0; font-size:13px; line-height:1.5; color:var(--paper-text); white-space:pre-line; }

/* The same small gold caption .form-legend gives, without its 30px margins,
   20px padding and rule — three of those were most of the wasted height. */
.q-note-label{
  display:block; font-family:var(--font-mono); font-size:10px;
  letter-spacing:.08em; text-transform:uppercase; color:var(--gold); margin-bottom:4px;
}
span.q-note-label{ margin-bottom:8px; }

.ledger .num{ text-align:right; font-family:var(--font-mono); white-space:nowrap; }
.ledger .name{ font-size:14px; }
.ledger .cat{ font-size:11px; color:var(--paper-dim); margin-top:3px; }

.q-totals{ margin:24px 0 6px; margin-left:auto; max-width:340px; }
.q-totals > div{ display:flex; justify-content:space-between; gap:24px; padding:9px 0; }
.q-totals dt{ color:var(--paper-dim); font-size:13px; }
.q-totals dd{ margin:0; font-size:14px; }
.q-totals .grand{ border-top:1px solid var(--paper-text); margin-top:6px; padding-top:13px; }
.q-totals .grand dt{ color:var(--paper-text); font-weight:600; }
.q-totals .grand dd{ font-size:17px; }
.q-vat{ text-align:right; font-size:12px; color:var(--paper-dim); margin:0 0 8px; }
.q-awaiting{ margin:24px 0 0; font-size:14px; color:var(--paper-dim); }

.q-actions{ display:flex; flex-wrap:wrap; gap:12px; align-items:center; margin-top:34px; padding-top:26px; border-top:1px solid var(--paper-line); }
.q-accepted{ font-size:12px; letter-spacing:.06em; text-transform:uppercase; color:#149c8b; }

/* --- the accept dialog ---
   Accepting is the only irreversible thing a customer can do on this site, so
   it gets a real modal: the page behind it is unusable while the question is
   open, Escape and the backdrop both cancel, and the confirm button does not
   light up until the authority checkbox is ticked. */
.modal-backdrop{
  position:fixed; inset:0; z-index:120;
  background:rgba(13,15,18,.72);
  display:none; align-items:center; justify-content:center; padding:24px;
}
.modal-backdrop.open{ display:flex; }
.modal-card{
  background:#fff; border:1px solid var(--paper-line); border-radius:6px;
  box-shadow:var(--shadow-lg); padding:34px;
  width:100%; max-width:520px; max-height:88vh; overflow-y:auto;
}
.modal-card h2{ font-size:26px; margin-bottom:12px; }
.modal-lead{ margin:0 0 20px; font-size:14px; color:var(--paper-text); }
.modal-figures{
  display:flex; justify-content:space-between; gap:20px;
  padding:14px 16px; margin-bottom:20px;
  background:var(--paper-soft); border:1px solid var(--paper-line); border-radius:3px;
  font-size:14px;
}
.modal-warn{
  margin:0 0 6px; padding:14px 16px;
  background:rgba(180,59,47,.06); border:1px solid rgba(180,59,47,.28); border-radius:3px;
  font-size:13px; color:var(--paper-text);
}
.modal-card .checkbox-row a{ color:var(--gold); text-decoration:underline; }
@media (max-width:640px){ .modal-card{ padding:24px 20px; } }

/* A warning notice is the same object as .gate-note with a different colour;
   the gold is for "here is something useful", the red for "this affects you". */
.gate-note.warn{ border-color:rgba(180,59,47,.5); background:rgba(180,59,47,.05); }
.gate-note.warn .g-ico{ color:var(--red-stamp); }

/* --- offline preview bar (demo.html only) ---
   Styled to look like tooling rather than like the site, so a screenshot of
   the preview can never be mistaken for the real dashboard. */
.demo-bar{
  position:fixed; left:0; right:0; bottom:0; z-index:200;
  display:flex; flex-wrap:wrap; align-items:center; gap:8px;
  padding:10px 14px; background:#111418; border-top:2px solid var(--gold);
  font-family:var(--font-mono); font-size:11px; color:var(--ink-dim);
}
.demo-bar strong{ color:var(--gold); letter-spacing:.14em; }
.demo-bar span{ margin-right:10px; }
.demo-bar button{
  background:none; border:1px solid var(--ink-line); color:var(--ink-dim);
  padding:5px 10px; border-radius:2px; font-family:inherit; font-size:11px;
}
.demo-bar button:hover{ color:var(--ink-text); border-color:var(--ink-text); }
.demo-bar button.on{ color:#171204; background:var(--gold); border-color:var(--gold); }
body:has(.demo-bar) .site-footer{ padding-bottom:90px; }

/* ==========================================================================
   Reveal on scroll — visible-by-default failsafe pattern
   ========================================================================== */
.reveal, .reveal-stagger > *{
  opacity:1;
  transform:none;
  transition:opacity .7s var(--ease), transform .7s var(--ease);
}
/* The clip exists to stop the 20px rise poking out of the section above, so
   it only belongs to the pre-reveal state. Leaving it on permanently gave
   every revealed element a clipped raster layer for the rest of the page's
   life, which is dead weight on a phone. */
.reveal.pre, .reveal-stagger.pre > *{
  clip-path:inset(0 0 -6% 0);
  opacity:0;
  transform:translateY(20px);
}
.reveal-stagger.pre > *:nth-child(2){ transition-delay:.08s; }
.reveal-stagger.pre > *:nth-child(3){ transition-delay:.16s; }
.reveal-stagger.pre > *:nth-child(4){ transition-delay:.24s; }
@media (prefers-reduced-motion: reduce){
  .reveal, .reveal-stagger > *{ transition:none !important; opacity:1 !important; transform:none !important; }
}

/* ==========================================================================
   Footer
   ========================================================================== */
.site-footer{ background:var(--ink); color:var(--ink-dim); padding:70px 0 30px; }
.footer-grid{ display:grid; grid-template-columns:1.4fr 1fr 1fr 1fr 1fr; gap:40px; margin-bottom:56px; }
@media (max-width:980px){ .footer-grid{ grid-template-columns:1fr 1fr 1fr; } }
@media (max-width:820px){ .footer-grid{ grid-template-columns:1fr 1fr; } }
@media (max-width:520px){ .footer-grid{ grid-template-columns:1fr; } }
.footer-grid h4{ font-family:var(--font-mono); font-size:11px; text-transform:uppercase; letter-spacing:.08em; color:var(--gold); margin-bottom:16px; }
.footer-grid p{ font-size:13px; max-width:32ch; }
.footer-tagline{ color:var(--ink-dim); line-height:1.5; }
.footer-grid li{ margin-bottom:10px; font-size:13px; }
.footer-grid a:hover{ color:var(--gold); }
.footer-social{ display:flex; gap:10px; margin-top:14px; list-style:none; }
.footer-social-icon{ display:inline-flex; align-items:center; justify-content:center; width:30px; height:30px; border:1px solid var(--ink-line); border-radius:50%; color:var(--ink-dim); transition:color .25s, border-color .25s; }
.footer-social-icon:hover{ color:var(--gold); border-color:var(--gold); }
.footer-bottom{
  display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:12px;
  border-top:1px solid var(--ink-line); padding-top:26px; font-size:12px; font-family:var(--font-mono);
}

/* generic content pages */
/* The dark band at the top of every page but the homepage.

   Deliberately short. It used to open with 64px of padding above a 52px
   heading, which on a page whose point is the content below it — a quote, a
   returns policy, a list of items — was most of a phone screen spent saying
   where you already knew you were. These are the proportions the dashboard's
   detail view was cut down to, applied everywhere.

   The homepage is untouched: that hero is .hero, a different element with a
   different job. */
.page-hero{ padding:30px 0 24px; }
.page-hero h1{ font-size:clamp(22px,2.8vw,30px); }
.page-hero p{ color:var(--ink-dim); max-width:62ch; margin-top:9px; font-size:15px; }
.two-col{ display:grid; grid-template-columns:1fr 1fr; gap:60px; align-items:start; }
@media (max-width:860px){ .two-col{ grid-template-columns:1fr; } }
/* Stacks unconditionally, at every width — for the one .two-col instance
   (homepage closing CTA) the owner wants vertical always, not just on
   narrow screens. Higher specificity than the media query above, so no
   !important needed. */
.two-col.stack-always{ grid-template-columns:1fr; gap:32px; }

.legal-copy h2{ font-size:22px; margin-top:44px; margin-bottom:14px; }
.legal-copy h2:first-child{ margin-top:0; }
.legal-copy p{ color:var(--paper-dim); margin-bottom:14px; }
.legal-copy p a{ text-decoration:underline; text-underline-offset:2px; color:var(--paper-text); }
.legal-copy p a:hover{ color:var(--gold); }

/* Product gallery — thumbnail strip under the main photo on the detail page. */
.product-detail-figure{ min-width:0; }
.product-thumbs{ display:flex; flex-wrap:wrap; gap:10px; margin-top:12px; }
.product-thumb{ flex:0 0 auto; width:74px; aspect-ratio:1/1; padding:0; background:#fff; border:1px solid var(--paper-line); border-radius:10px; overflow:hidden; cursor:pointer; transition:border-color .18s ease, transform .18s ease; }
.product-thumb img{ display:block; width:100%; height:100%; object-fit:contain; padding:6px; }
.product-thumb:hover{ transform:translateY(-2px); }
.product-thumb.active{ border-color:var(--gold); }
@media (max-width:640px){ .product-thumb{ width:62px; } }
/* --------------------------------------------------------------------------
   Back to top — fixed at the top right, under the header, every breakpoint.
   Injected by initBackToTop(); that function owns .is-visible.
   -------------------------------------------------------------------------- */
.to-top{
  position:fixed;
  /* Clear of the 76px sticky header, and below it in the stack: the two never
     overlap, but the header must win if a zoomed viewport ever makes them. */
  top:92px;
  right:max(20px, env(safe-area-inset-right));
  z-index:190;
  display:inline-flex; align-items:center; justify-content:center;
  width:46px; height:46px;
  border-radius:50%;
  background:rgba(13,15,18,.88);
  color:var(--ink-text);
  border:1px solid var(--ink-line);
  backdrop-filter:blur(8px);
  box-shadow:0 8px 22px rgba(13,15,18,.3);
  /* Hidden until there is somewhere to go back to. visibility rides along so
     the button leaves the tab order while it is invisible. */
  opacity:0;
  visibility:hidden;
  transform:translateY(-8px) scale(.94);
  transition:opacity .3s var(--ease), transform .3s var(--ease),
             visibility .3s, background .25s var(--ease), color .25s var(--ease);
}
.to-top.is-visible{ opacity:1; visibility:visible; transform:none; }
.to-top svg{
  width:20px; height:20px;
  fill:none; stroke:currentColor;
  stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round;
}
.to-top:hover{ background:var(--gold); color:var(--ink); border-color:var(--gold); }
.to-top:active{ transform:scale(.93); }
/* The off-canvas menu covers the screen — a button floating on top of it is
   just clutter over the nav's own controls. */
body.nav-open .to-top{ opacity:0; visibility:hidden; pointer-events:none; }
@media (max-width:640px){
  .to-top{
    top:88px;
    right:max(14px, env(safe-area-inset-right));
    width:42px; height:42px;
  }
  .to-top svg{ width:18px; height:18px; }
}
@media (prefers-reduced-motion:reduce){
  .to-top{ transition:opacity .01s, visibility .01s; }
}

/* --------------------------------------------------------------------------
   Floating quote button — mobile only
   The header basket is display:none below 940px (it moves into the off-canvas
   menu), so on a phone there is no persistent way back to the quote list. This
   fills that gap. Same 940px breakpoint as the nav, so the two never overlap
   in responsibility. Injected by initMobileQuoteFab().
   -------------------------------------------------------------------------- */
.quote-fab{ display:none; }
@media (max-width:940px){
  .quote-fab{
    display:inline-flex; align-items:center; justify-content:center;
    position:fixed; z-index:60;
    right:max(18px, env(safe-area-inset-right));
    bottom:max(18px, env(safe-area-inset-bottom));
    width:52px; height:52px; border-radius:50%;
    background:var(--ink); color:var(--ink-text);
    border:1px solid var(--ink-line);
    box-shadow:0 6px 20px rgba(13,15,18,.28);
    transition:transform .18s var(--ease), opacity .18s ease;
  }
  .quote-fab svg{
    width:22px; height:22px; fill:none; stroke:currentColor;
    stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round;
  }
  .quote-fab circle{ fill:currentColor; stroke:none; }
  .quote-fab:active{ transform:scale(.94); }
  .quote-fab .quote-badge{
    position:absolute; top:-2px; right:-2px;
    width:19px; height:19px; font-size:10px;
    border:2px solid var(--ink);
  }
  /* The off-canvas menu has its own basket row, and the panel covers the
     screen — leaving the button floating on top of it would be a duplicate. */
  body.nav-open .quote-fab{ opacity:0; pointer-events:none; }
}
@media (prefers-reduced-motion:reduce){
  .quote-fab{ transition:none; }
}

/* ---------------------------------------------------------------------
   14. Entrance intro — bouncing Poké Ball (index.html only)
   ---------------------------------------------------------------------
   A ~1.5s black overlay that is pure theatre. It deliberately measures
   nothing: no image, font, CDN script or fetch is waited on. The page boots
   underneath it at full speed and the overlay leaves on its own schedule
   whether the rest arrived or not — see the controller in index.html's
   <head>, which only listens for `introReveal` starting and ending.

   Everything here runs on transform, opacity and one masked radius, so the
   whole sequence stays on the compositor. All timing lives in this file;
   the JS holds no durations of its own.

   Timeline (from first paint):
     0.06s  ball fades in, held one ball-height above the ground
     0.12s  drop begins            0.34s  first impact + squash
     0.59s  top of the bounce      0.90s  second, smaller impact
     0.04s  progress bar starts    1.20s  bar reaches full
     1.14s  circular reveal opens from the ball's centre
     1.54s  overlay gone
   --------------------------------------------------------------------- */

/* The reveal is a hole punched through the black with a radial-gradient
   mask. Only a *registered* custom property interpolates, hence @property;
   where that is unsupported the radius simply stays at 0 and the opacity
   fade built into the same keyframes carries the transition instead. */
@property --intro-r{
  syntax:"<length>";
  inherits:false;
  initial-value:0px;
}

/* Inert unless the <head> controller opted this load in — a returning visit
   inside the same session never renders it at all. */
.intro{ display:none; }

html.intro-active .intro{
  --ball:clamp(64px, 15vw, 84px);
  --drop:calc(var(--ball) * -1);   /* the ball starts one diameter up */
  display:block;
  position:fixed; inset:0; z-index:9000;
  background:#000;
  /* The overlay has nothing to scroll and the page underneath shouldn't move
     while it is up; the controller blocks wheel/touch for the same reason. */
  touch-action:none;
  overscroll-behavior:contain;
  /* The base layer of the dissolve — the slowest and softest of the five,
     so it is the last thing to clear and its animationend is what the
     controller in index.html treats as "the intro is over". */
  -webkit-mask-image:radial-gradient(circle at 50% 50%, transparent var(--intro-r), #000 calc(var(--intro-r) * 2.6 + 1px));
          mask-image:radial-gradient(circle at 50% 50%, transparent var(--intro-r), #000 calc(var(--intro-r) * 2.6 + 1px));
  animation:introReveal 1.75s cubic-bezier(.5,.02,.4,1) 1.18s forwards;
}
/* 95vmax clears the half-diagonal of any aspect ratio (worst case ~71vmax)
   even after the feather is subtracted, so the opening always finishes
   larger than the viewport. The feather is a *multiple* of the radius, not a
   fixed length: at r=0 both gradient stops sit at 0, so the screen starts
   completely black and the edge only softens as the hole grows. */
@keyframes introReveal{
  0%   { --intro-r:0px;     opacity:1; }
  55%  {                    opacity:1; }
  100% { --intro-r:95vmax;  opacity:0; }
}

/* --- The other four veils. Same idea, stacked, each a beat later and a
   little lumpier than the one under it. Two extra off-centre gradients per
   layer break the perfect circle, and a real blur turns each edge into a
   wisp; the stagger is what makes the black come apart layer by layer
   instead of all at once. inset:-14% keeps the blurred edge of the layer
   itself off screen. --- */
html.intro-active .intro-veil{
  position:absolute; inset:-14%;
  display:block;
  background:#000;
  will-change:mask-image, opacity;
  animation:introReveal 1.5s cubic-bezier(.5,.02,.42,1) 1.14s forwards;
}
html.intro-active .intro-veil.v1{
  filter:blur(10px);
  animation-delay:1.02s; animation-duration:1.20s;
  -webkit-mask-image:radial-gradient(circle at 50% 50%, transparent var(--intro-r), #000 calc(var(--intro-r) * 1.7 + 1px));
          mask-image:radial-gradient(circle at 50% 50%, transparent var(--intro-r), #000 calc(var(--intro-r) * 1.7 + 1px));
}
html.intro-active .intro-veil.v2{
  filter:blur(16px);
  animation-delay:1.10s; animation-duration:1.32s;
  -webkit-mask-image:
    radial-gradient(circle at 46% 53%, transparent var(--intro-r), #000 calc(var(--intro-r) * 2 + 1px)),
    radial-gradient(circle at 56% 44%, transparent calc(var(--intro-r) * .8), #000 calc(var(--intro-r) * 1.9 + 1px));
          mask-image:
    radial-gradient(circle at 46% 53%, transparent var(--intro-r), #000 calc(var(--intro-r) * 2 + 1px)),
    radial-gradient(circle at 56% 44%, transparent calc(var(--intro-r) * .8), #000 calc(var(--intro-r) * 1.9 + 1px));
  -webkit-mask-composite:source-over;
          mask-composite:add;
}
html.intro-active .intro-veil.v3{
  filter:blur(24px);
  animation-delay:1.22s; animation-duration:1.45s;
  -webkit-mask-image:
    radial-gradient(circle at 54% 47%, transparent var(--intro-r), #000 calc(var(--intro-r) * 2.2 + 1px)),
    radial-gradient(circle at 43% 58%, transparent calc(var(--intro-r) * .7), #000 calc(var(--intro-r) * 2.1 + 1px));
          mask-image:
    radial-gradient(circle at 54% 47%, transparent var(--intro-r), #000 calc(var(--intro-r) * 2.2 + 1px)),
    radial-gradient(circle at 43% 58%, transparent calc(var(--intro-r) * .7), #000 calc(var(--intro-r) * 2.1 + 1px));
  -webkit-mask-composite:source-over;
          mask-composite:add;
}
html.intro-active .intro-veil.v4{
  filter:blur(34px);
  animation-delay:1.34s; animation-duration:1.58s;
  -webkit-mask-image:
    radial-gradient(circle at 50% 50%, transparent var(--intro-r), #000 calc(var(--intro-r) * 2.4 + 1px)),
    radial-gradient(circle at 60% 60%, transparent calc(var(--intro-r) * .6), #000 calc(var(--intro-r) * 2.3 + 1px));
          mask-image:
    radial-gradient(circle at 50% 50%, transparent var(--intro-r), #000 calc(var(--intro-r) * 2.4 + 1px)),
    radial-gradient(circle at 60% 60%, transparent calc(var(--intro-r) * .6), #000 calc(var(--intro-r) * 2.3 + 1px));
  -webkit-mask-composite:source-over;
          mask-composite:add;
}

/* --- Nothing on the page shows through the dissolve. Everything above the
   fold is held at zero until the black is completely gone, and then arrives
   as a cascade: logo, nav, header tools, headline, lead, each button, the
   card scene, the scroll cue — reading order, top to bottom. Each step is
   short (.5s) and they overlap, so it lands as one quick sweep down the
   screen rather than eight separate entrances.

   The delays are derived from one number, --introStep, so the whole cascade
   can be tightened or loosened in a single edit.

   Without the .intro-in half of this pair the page is simply itself — a
   browser that never ran the overlay, or reduce-motion, never sees a
   hidden header. --- */
html.intro-hold .logo,
html.intro-hold .main-nav > a,
html.intro-hold .header-tools,
html.intro-hold .hero h1,
html.intro-hold .hero-lead,
html.intro-hold .hero-actions > *,
html.intro-hold .hero-card-stage,
html.intro-hold .hero-scroll-wrap{
  opacity:0;
}
html.intro-in{ --introStep:.07s; }
html.intro-in .logo,
html.intro-in .main-nav > a,
html.intro-in .header-tools,
html.intro-in .hero h1,
html.intro-in .hero-lead,
html.intro-in .hero-actions > *,
html.intro-in .hero-card-stage,
html.intro-in .hero-scroll-wrap{
  animation:introFadeUp .5s ease-out both;
  /* Each of these gets its own compositor layer for the length of the
     cascade, so the fades are composited rather than repainting text and the
     card scene on the main thread frame by frame. The boot work happening
     alongside them (§13) can then no longer stutter them. It is scoped to
     .intro-in, so the layers exist only while the cascade is playing and are
     dropped with the class — a permanent will-change here would pin a layer
     per element for the rest of the page's life. */
  will-change:opacity;
}
html.intro-in .logo             { animation-delay:0s; }
html.intro-in .main-nav > a:nth-child(1){ animation-delay:calc(var(--introStep) * 1); }
html.intro-in .main-nav > a:nth-child(2){ animation-delay:calc(var(--introStep) * 1.6); }
html.intro-in .main-nav > a:nth-child(3){ animation-delay:calc(var(--introStep) * 2.2); }
html.intro-in .main-nav > a:nth-child(4){ animation-delay:calc(var(--introStep) * 2.8); }
html.intro-in .header-tools     { animation-delay:calc(var(--introStep) * 3.4); }
html.intro-in .hero h1          { animation-delay:calc(var(--introStep) * 4.4); }
html.intro-in .hero-lead        { animation-delay:calc(var(--introStep) * 5.6); }
html.intro-in .hero-actions > *:nth-child(1){ animation-delay:calc(var(--introStep) * 6.6); }
html.intro-in .hero-actions > *:nth-child(2){ animation-delay:calc(var(--introStep) * 7.2); }
/* The card scene's own cards are dealt in by GSAP (js/main.js §5b), which is
   held back by the same amount — this only fades the stage they sit on. */
html.intro-in .hero-card-stage  { animation-delay:calc(var(--introStep) * 7.8); }
html.intro-in .hero-scroll-wrap { animation-delay:calc(var(--introStep) * 9); }
/* The gold sweep is a background-position animation on background-clip:text,
   which means it re-rasterises the glyphs on the main thread every single
   frame. Running that inside the word that is itself fading up is what made
   the cascade stutter on a real first load. It waits for the cascade to
   finish instead — .intro-in is dropped by releaseIntroCascade() (main.js
   §13), and the sweep starts then, on an idle page, where it costs nothing
   anyone can see.

   .intro-active matters just as much and is easy to miss: that is the
   dissolve, and the dissolve is four stacked veils each opening a blurred
   radial hole over the whole viewport. Everything underneath is recomposited
   behind them every frame, so anything that also repaints per frame is paid
   for twice. On a refresh the browser restores the scroll position, so the
   word under the overlay is whichever one the visitor was last looking at —
   which is why this showed on the closing CTA's "Jouw marge blijft retail"
   but never on the hero headline, where intro-hold's opacity:0 means it is
   not painted at all. */
html.intro-active .shine,
html.intro-in .shine{ animation:none; }

/* Same reasoning, other mechanism: the closing mascot is a 12-frame animated
   WebP, so it decodes and repaints on its own clock for as long as it is
   rendered — no CSS animation to switch off. Taking it out of rendering for
   the length of the dissolve is what stops it. It is absolutely positioned
   and decorative, so nothing moves when it goes. */
html.intro-active .closing-mascot{ display:none; }

/* And the third one, which is about the compositor rather than about paint.
   .book-page and .card-rain img both carry a permanent will-change, so the
   book section alone is five viewport-sized layers (each with a 70px-blur
   box-shadow) plus sixteen card layers. The dissolve is itself five masked,
   blurred, full-viewport layers — stack the two and the compositor is moving
   twenty-odd viewport-scale surfaces per frame, which is why a refresh with
   the book on screen stuttered while the same refresh on the hero or the
   feature cards (where those layers are off screen) did not.
   Nothing underneath is animating during the dissolve — initBook and the rain
   are both deferred past it — so the promotions buy nothing here and are
   handed back for its duration. */
html.intro-active .book-page,
html.intro-active .card-rain img{ will-change:auto; }

/* Opacity only. Nothing travels: with eight elements coming in one after
   the other, any shared vertical offset reads as a roller blind coming down
   the page rather than as eight things appearing. The sequence alone carries
   the top-to-bottom direction. */
@keyframes introFadeUp{
  from{ opacity:0; }
  to  { opacity:1; }
}
@media (max-width:940px){
  /* The nav is collapsed behind the toggle here, so its links are not part
     of the cascade and the copy should not wait for them. */
  html.intro-in{ --introStep:.06s; }
  html.intro-in .main-nav > a{ animation:none; opacity:1; transform:none; }
  html.intro-in .header-tools{ animation-delay:calc(var(--introStep) * 1); }
  html.intro-in .hero h1     { animation-delay:calc(var(--introStep) * 2); }
  html.intro-in .hero-lead   { animation-delay:calc(var(--introStep) * 3.2); }
  html.intro-in .hero-actions > *:nth-child(1){ animation-delay:calc(var(--introStep) * 4.2); }
  html.intro-in .hero-actions > *:nth-child(2){ animation-delay:calc(var(--introStep) * 4.8); }
  html.intro-in .hero-card-stage { animation-delay:calc(var(--introStep) * 5.4); }
  html.intro-in .hero-scroll-wrap{ animation-delay:calc(var(--introStep) * 6.2); }
}

/* The ball sits on the exact centre of the viewport, so the reveal opens
   from the ball rather than from the middle of ball + bar as a group. */
.intro-stage{
  position:absolute; left:50%; top:50%;
  width:var(--ball); height:var(--ball);
  margin:calc(var(--ball) / -2) 0 0 calc(var(--ball) / -2);
  animation:
    introStageIn  .16s linear .06s both,
    introStageOut .55s cubic-bezier(.5,0,.75,1) 1.14s forwards;
}
@keyframes introStageIn{ from{ opacity:0; } to{ opacity:1; } }
/* Pushes through the ball as the hole opens; the mask, not this fade, is
   what actually removes it — the opacity is only there so the fallback
   path (no @property) still has something to leave on. */
@keyframes introStageOut{
  0%   { transform:scale(1);   opacity:1; }
  70%  {                       opacity:1; }
  100% { transform:scale(1.9); opacity:0; }
}

/* Three jobs, three elements, so no two animations fight over one transform:
   .intro-drop = the bounce, .intro-ball = the squash, .intro-shadow = the
   contact patch (which stays on the ground and never travels with the ball). */
.intro-drop{
  position:absolute; inset:0; display:block;
  will-change:transform;
  animation:introDrop .90s .12s both;
}
@keyframes introDrop{
  0%   { transform:translateY(var(--drop));               animation-timing-function:cubic-bezier(.45,0,.85,.6); }
  24%  { transform:translateY(0);                         animation-timing-function:cubic-bezier(.2,.6,.4,1); }
  52%  { transform:translateY(calc(var(--drop) * .40));   animation-timing-function:cubic-bezier(.5,0,.85,.55); }
  78%  { transform:translateY(0);                         animation-timing-function:cubic-bezier(.2,.6,.4,1); }
  90%  { transform:translateY(calc(var(--drop) * .12));   animation-timing-function:cubic-bezier(.5,0,.9,.6); }
  100% { transform:translateY(0); }
}

.intro-ball{
  display:block; width:100%; height:100%;
  transform-origin:50% 100%;   /* squash from the contact point, not the middle */
  will-change:transform;
  animation:introSquash .90s .12s both;
}
@keyframes introSquash{
  0%,20%   { transform:scale(1,1); }
  25%      { transform:scale(1.16,.84); }
  33%      { transform:scale(.95,1.05); }
  44%,74%  { transform:scale(1,1); }
  79%      { transform:scale(1.09,.91); }
  86%      { transform:scale(.98,1.02); }
  94%,100% { transform:scale(1,1); }
}

/* On a black field a shadow can only read as a faint light contact patch —
   kept well under a tenth of full white so it never becomes a glow. */
.intro-shadow{
  position:absolute; left:50%; top:calc(100% + 7px);
  width:var(--ball); height:calc(var(--ball) * .17);
  margin-left:calc(var(--ball) / -2);
  border-radius:50%;
  background:rgba(255,255,255,.55);
  filter:blur(5px);
  opacity:.12;
  will-change:transform, opacity;
  animation:introShadow .90s linear .12s both;
}
@keyframes introShadow{
  0%   { transform:scale(.5,.5);   opacity:.05; }
  24%  { transform:scale(1.1,1);   opacity:.15; }
  33%  { transform:scale(1,1);     opacity:.12; }
  52%  { transform:scale(.68,.9);  opacity:.07; }
  78%  { transform:scale(1.06,1);  opacity:.14; }
  90%  { transform:scale(.86,.95); opacity:.10; }
  100% { transform:scale(1,1);     opacity:.12; }
}

/* Deliberately no percentage, no counter and no "Loading…" — it measures
   nothing, so it must not claim to. */
.intro-bar{
  position:absolute; left:50%; top:50%;
  transform:translateX(-50%);
  margin-top:calc(var(--ball) * .5 + clamp(34px, 7vw, 44px));
  width:clamp(140px, 46vw, 200px); height:3px;
  border-radius:999px;
  background:rgba(255,255,255,.13);
  overflow:hidden;
  animation:introFadeOut .22s ease 1.06s forwards;
}
.intro-bar-fill{
  display:block; height:100%; width:100%;
  border-radius:inherit;
  background:rgba(255,255,255,.92);
  transform:scaleX(0); transform-origin:0 50%;
  will-change:transform;
  animation:introBar 1.16s cubic-bezier(.45,.15,.2,1) .04s forwards;
}
@keyframes introBar{ from{ transform:scaleX(0); } to{ transform:scaleX(1); } }
@keyframes introFadeOut{ to{ opacity:0; } }

@media (prefers-reduced-motion: reduce){
  /* Same animation *name* on the overlay — the controller keys off it, so
     renaming it here would strand the overlay on screen until the safety
     timeout. Only its shape changes: a short fade, no travelling mask. */
  @keyframes introReveal{ 0%{ opacity:1; } 100%{ opacity:0; } }
  html.intro-active .intro{
    -webkit-mask-image:none; mask-image:none;
    animation-duration:.26s; animation-delay:.30s; animation-timing-function:linear;
  }
  html.intro-active .intro-drop,
  html.intro-active .intro-ball,
  html.intro-active .intro-shadow,
  html.intro-active .intro-bar{ animation:none; }
  html.intro-active .intro-stage{ animation:introStageIn .12s linear both; }
  html.intro-active .intro-bar-fill{ animation-duration:.30s; animation-delay:0s; }
  /* The layered dissolve is the one thing that cannot be toned down — it IS
     motion — so the extra veils are simply not drawn. */
  html.intro-active .intro-veil{ display:none; }
  /* The page still waits for the overlay, but arrives all at once and
     without travel — a cascade is motion, which is the thing being opted
     out of here. */
  html.intro-in .logo,
  html.intro-in .main-nav > a,
  html.intro-in .header-tools,
  html.intro-in .hero h1,
  html.intro-in .hero-lead,
  html.intro-in .hero-actions > *,
  html.intro-in .hero-card-stage,
  html.intro-in .hero-scroll-wrap{
    animation:introFade .25s linear both;
    animation-delay:0s !important;
  }
  @keyframes introFade{ from{ opacity:0; } to{ opacity:1; } }
}

/* ---------------------------------------------------------------------
   15. Hero scroll-down button
   ---------------------------------------------------------------------
   Sits in normal flow at the foot of the hero rather than being pinned to
   the viewport, so it keeps the hero's own rhythm at every breakpoint.
   Only the arrow ever moves: the capsule, its border and the label are
   fixed, which is what keeps the invitation subtle instead of restless.
   --------------------------------------------------------------------- */
/* Pinned to the very bottom edge of the hero (matching the reference): the
   capsule sits flush on the hero's lower boundary, centred, so it reads as
   the doorway out of the hero rather than as one more element in the stack.
   The hero itself carries extra bottom padding so nothing collides with it. */
.hero-scroll-wrap{
  display:flex; justify-content:center;
  position:absolute;
  left:0; right:0; bottom:0;
  z-index:3;
  pointer-events:none;              /* only the button itself is clickable */
}
.hero-scroll-wrap > *{ pointer-events:auto; }
.hero-scroll{
  display:inline-flex; align-items:center; justify-content:center;
  gap:9px;
  min-height:42px;
  padding:11px 22px 12px;
  /* Sits flush on the hero's bottom edge and rises out of it: only the top
     corners are rounded, at the dynamic island's radius. Rounding the bottom
     as well would leave two visible notches against the edge. */
  border-radius:21px 21px 0 0;
  border:1px solid rgba(236,234,227,.10);
  border-bottom:none;
  background:rgba(10,12,15,.55);
  -webkit-backdrop-filter:blur(10px) saturate(1.2);
          backdrop-filter:blur(10px) saturate(1.2);
  /* Deliberately quiet: it is a cue, not a call to action. */
  color:rgba(236,234,227,.72);
  font-family:var(--font-mono);
  font-size:10.5px;
  letter-spacing:.12em;
  text-transform:uppercase;
  text-indent:.12em;                /* optical centring, as on .btn */
  cursor:pointer;
  /* Enters with the hero (see initHeroScroll); the colour transitions are
     the hover state and are unaffected by the entrance. */
  opacity:0; transform:translateY(10px);
  transition:opacity .6s var(--ease), transform .6s var(--ease),
             background .3s var(--ease), border-color .3s var(--ease),
             color .3s var(--ease);
}
.hero-scroll.is-in{ opacity:1; transform:none; }
/* Scrolling has started: drop it straight down through the hero's bottom
   edge rather than fading in place — it reads as the cue getting out of the
   way. Wins over .is-in because it comes later at equal specificity. */
.hero-scroll.is-in.is-gone{
  opacity:0;
  transform:translateY(100%);
  pointer-events:none;
  /* Safari (iOS especially) keeps compositing a backdrop-filter's blurred
     backdrop even at opacity:0, so on a phone the capsule stayed on screen
     as a ghost after scrolling started while desktop hid it correctly.
     Dropping the filter and taking the element out of rendering once the
     transition has finished is what actually removes it. `visibility` is
     transitionable, so the fade still plays before it goes. */
  -webkit-backdrop-filter:none;
          backdrop-filter:none;
  visibility:hidden;
  transition:opacity .25s var(--ease), transform .35s var(--ease),
             visibility 0s linear .35s;
}
.hero-scroll-arrow{
  display:inline-flex;
  order:-1;                         /* arrow leads the label, as in the island */
  transition:transform .3s var(--ease);
}
.hero-scroll-arrow svg{
  display:block; width:12px; height:12px;
  fill:none; stroke:currentColor; stroke-width:1.8;
  stroke-linecap:round; stroke-linejoin:round;
}
/* One bounce, then a long rest — a continuously moving arrow reads as a
   loading state rather than an invitation. */
@keyframes heroScrollArrow{
  0%,16%   { transform:translateY(0);   animation-timing-function:cubic-bezier(.4,0,.2,1); }
  30%      { transform:translateY(5px); animation-timing-function:cubic-bezier(.3,1.3,.5,1); }
  46%,100% { transform:translateY(0); }
}
.hero-scroll.is-in .hero-scroll-arrow svg{
  animation:heroScrollArrow 1.5s infinite;
}
@media (hover:hover){
  .hero-scroll:hover{
    background:rgba(10,12,15,.72);
    border-color:rgba(236,234,227,.22);
    color:var(--ink-text);
  }
  .hero-scroll:hover .hero-scroll-arrow{ transform:translateY(3px); }
}
.hero-scroll:active{ background:rgba(10,12,15,.85); }
@media (max-width:760px){
  /* A touch tighter so it clears the quote FAB in the bottom-right corner
     on narrow phones. */
  .hero-scroll{ padding:10px 18px 11px; min-height:38px; font-size:10px; letter-spacing:.1em; border-radius:19px 19px 0 0; }
}
@media (max-width:640px){
  .hero-scroll{ min-height:38px; padding:11px 18px; font-size:10.5px; gap:10px; }
}
@media (prefers-reduced-motion: reduce){
  .hero-scroll{ transition:background .3s, border-color .3s; opacity:1; transform:none; }
  .hero-scroll.is-in .hero-scroll-arrow svg{ animation:none; }
  .hero-scroll-arrow{ transition:none; }
}

/* ==========================================================================
   16. Poké Ball shutter — "Wat bieden we?" is opened by the visitor.

   Not a ball on top of the page: a full-width Poké Ball lid lying over the
   section and nothing else — red panel above, paper panel below, one dark
   seam between them and the button sitting on that seam. It is absolutely
   positioned inside the section rather than stuck to the viewport, so it
   covers exactly this one hero and scrolls away with it; a sticky lid stayed
   glued to the screen and read as if it had taken over the whole site. Deliberately flat: solid fills, no
   gradients, no highlights, no shadows. The only light in the whole thing is
   the button, because the button is the one thing the visitor has to notice.

   Built by initPokeballShutter() (main.js §7d) and only when GSAP is there
   and motion is allowed — without it nothing is inserted and the section is
   the plain one it always was.
   ========================================================================== */
.feature{ position:relative; }
.feature-head{ text-align:center; position:relative; }
.feature-head h2{ margin-inline:auto; }
.feature > .container{ position:relative; z-index:1; }

.pokeball-reveal{
  position:absolute;
  inset:0;                /* exactly the section — no more, no less */
  z-index:3;
  overflow:hidden;        /* the panels leave through the viewport edges */
  pointer-events:none;    /* only the button is clickable — see .pb-eye */
  /* Declared here rather than on the button: the label positions itself
     against the button's size too, and a custom property set on the button
     would not reach a sibling. */
  --pb-eye-size:clamp(58px, 6.6vw, 92px);
}

/* The two panels. Full-bleed, flat colour, and the seam is a border on each
   of them, so when they part each takes its own half of it with it — the
   seam can never be left hanging off one panel. */
.pokeball-half{
  position:absolute;
  left:0; width:100%; height:50%;
  will-change:transform;
}
.pokeball-top{
  top:0;
  background:var(--pb-red);
  border-bottom:var(--pb-seam) solid var(--ink);
}
.pokeball-bottom{
  bottom:0;
  background:var(--pb-shell);
  border-top:var(--pb-seam) solid var(--ink);
}
:root{
  --pb-red:   #D0342C;   /* flat, no gradient — the shell is one colour */
  --pb-shell: #F2F0EA;   /* paper-white, so the lid belongs to this palette */
  --pb-seam:  clamp(7px, 0.9vw, 12px);
  /* The round seam reads thinner than the straight one at the same width —
     it is a ring, so only its outer edge is ever against the shell. A touch
     more weight puts the two back in balance. */
  --pb-ring:  clamp(9px, 1.15vw, 15px);
}

/* --- The button. The only lit element, and the only clickable one. --- */
.pb-eye{
  position:absolute;
  left:50%; top:50%;
  width:var(--pb-eye-size); height:var(--pb-eye-size);
  margin:0; padding:0;
  transform:translate3d(-50%,-50%,0);
  border:var(--pb-ring) solid var(--ink);
  border-radius:50%;
  background:var(--pb-shell);
  cursor:pointer;
  pointer-events:auto;
  -webkit-tap-highlight-color:transparent;
  /* The glow lives on the button and nowhere else. */
  animation:pbEyeBreathe 2.6s ease-in-out infinite;
}
@keyframes pbEyeBreathe{
  0%,100%{ background:var(--pb-shell); box-shadow:0 0 0 0 rgba(228,188,62,0); }
  50%    { background:#FFF6D8;         box-shadow:0 0 clamp(22px,3vw,40px) clamp(4px,.6vw,9px) rgba(228,188,62,.55); }
}

/* The radar ring: one calm pulse outward, not a constant halo. */
.pb-eye::after{
  content:"";
  position:absolute;
  inset:calc(-1 * var(--pb-ring));
  border:2px solid rgba(228,188,62,.85);
  border-radius:50%;
  animation:pbEyeRing 2.6s ease-out infinite;
  pointer-events:none;
}
@keyframes pbEyeRing{
  0%  { transform:scale(1);   opacity:.75; }
  70% { transform:scale(2.1); opacity:0; }
  100%{ transform:scale(2.1); opacity:0; }
}

/* Label, in the site's own mono voice. Sits below the button, on the paper
   panel, so it is dark-on-light and needs no plate behind it. */
.pb-hint{
  position:absolute;
  left:50%; top:calc(50% + var(--pb-eye-size) * .5 + clamp(18px, 2.4vw, 30px));
  transform:translate3d(-50%,0,0);
  font-family:var(--font-mono);
  font-size:clamp(10px, 1.05vw, 12px);
  letter-spacing:.18em;
  text-transform:uppercase;
  color:var(--paper-text);
  white-space:nowrap;
  pointer-events:none;
  /* Bounces on the button's own beat: same 2.6s period, same easing, same
     start — so the label lifts exactly as the eye lights up and the ring
     goes out. The two read as one heartbeat rather than two loops that
     happen to be nearby. */
  animation:pbHintBounce 2.6s ease-in-out infinite;
}
@keyframes pbHintBounce{
  0%,100%{ transform:translate3d(-50%,0,0)            scale(1);    }
  38%    { transform:translate3d(-50%,-7px,0)         scale(1.04); }
  50%    { transform:translate3d(-50%,-9px,0)         scale(1.05); }
  66%    { transform:translate3d(-50%,0,0)            scale(1);    }
  74%    { transform:translate3d(-50%,-2.5px,0)       scale(1.01); }
}

@media (hover:hover){
  /* Hover confirms rather than decorates: the pulse runs faster and the ring
     is brighter, and nothing moves. */
  .pb-eye:hover{ animation-duration:1.3s; }
  .pb-eye:hover::after{ animation-duration:1.3s; border-color:rgba(228,188,62,1); }
  /* The label keeps time with it — the hint is a later sibling of the
     button, so it can follow the hover without any JS. */
  .pb-eye:hover ~ .pb-hint{ animation-duration:1.3s; }
}
.pb-eye:focus-visible{ outline:3px solid var(--gold-bright); outline-offset:6px; }
.pb-eye:active{ transform:translate3d(-50%,-50%,0) scale(.94); }

@media (max-width:640px){
  .pokeball-reveal{ --pb-eye-size:clamp(56px, 15vw, 74px); }
}
@media (prefers-reduced-motion: reduce){
  /* Nothing is inserted in this case, but if the layer is ever present the
     lid must not blink or pulse. */
  .pokeball-reveal{ display:none; }
}

/* --- The heading's rolling Poké Ball (main.js §7e). Flat, like the lid: two
   solid bands and a hard seam, no gradient shading and no shadow. It is
   positioned by JS in pixels measured from the heading's own text, so it is
   correct at any type size and in either language. --- */
.wwo-ball{
  position:absolute;
  left:0; top:0;
  width:var(--wwo-size); height:var(--wwo-size);
  --wwo-size:clamp(19px, 2vw, 28px);
  border-radius:50%;
  /* Hard colour stops — a two-tone shell, not a gradient. */
  background:linear-gradient(180deg,
    var(--pb-red)   0 45%,
    var(--ink)      45% 55%,
    var(--pb-shell) 55% 100%);
  box-shadow:inset 0 0 0 2px var(--ink);   /* flat outline, not a glow */
  opacity:0;
  pointer-events:none;
  will-change:transform;
}
.wwo-ball.is-rolling{ opacity:1; }
.wwo-ball::after{
  content:"";
  position:absolute;
  left:50%; top:50%;
  width:30%; height:30%;
  margin:-15% 0 0 -15%;
  border-radius:50%;
  background:var(--pb-shell);
  box-shadow:0 0 0 2px var(--ink);
}
@media (prefers-reduced-motion: reduce){
  .wwo-ball{ display:none; }
}
