:root {
  color-scheme: light;
  --ink: #1d1e1b;
  --paper: #f0eee8;
  --muted: #606058;
  --muted-on-accent: #a8a8a0;
  --line: rgba(29, 30, 27, .18);
  --acid: #3964c3;
  --on-acid: #f0eee8;
  --accent: #3964c3;
  --accent-on-accent: #9cb2e1;
  --accent-block-border: rgba(240, 238, 232, .5);
  --img-placeholder: #d8d6d0;
  --serif: Georgia, 'Times New Roman', serif;

  /* "accent surface" (about-cta, toast, tooltips) — always the OPPOSITE
     shade from the page around it, so it reads as a contrast block in
     both themes: dark-on-light here in light mode, swapped to
     light-on-dark in the dark-mode block further down. Leaving these
     values unchanged in dark mode would make accent-surface identical to
     --paper there (both #1d1e1b) and the block would vanish entirely —
     so dark mode gets its own inverted values below, not a copy of these. */
  --accent-surface: #1d1e1b;
  --on-accent-surface: #f0eee8;
  --accent-surface-border: rgba(240, 238, 232, .18);
  --status-error: #c0392b;
  --status-ok: #2f9e5b;
}

/* animated cross-fade between full page navigations (index/about/blank).
   Chromium-only today (Chrome/Edge) — unsupported browsers (Safari/Firefox)
   simply keep the normal instant navigation they already have, so this is
   a safe progressive enhancement with no fallback code needed. */
@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  /* the browser's default plus-lighter blend additively brightens the
     overlap between old/new snapshots, causing a white flash mid-crossfade;
     normal blending gives a plain smooth alpha crossfade instead */
  mix-blend-mode: normal;
}

/* "Focus pull" page change, 2026-09-24. Same idea as the fog reveal on the
   photos: the page you leave softly drifts out of focus and sinks back a
   touch, then the new page rises into focus. The old page is almost gone
   before the new one arrives, so the two never smear into each other.
   Only the snapshots animate (GPU work), never the live page. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) {
    animation: pageFocusOut .32s cubic-bezier(.4, 0, 1, 1) both;
  }
  ::view-transition-new(root) {
    animation: pageFocusIn .62s cubic-bezier(.16, 1, .3, 1) .12s both;
  }
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) { animation-duration: .2s; }
}
@keyframes pageFocusOut {
  to { opacity: 0; transform: scale(.985); filter: blur(6px); }
}
@keyframes pageFocusIn {
  from { opacity: 0; transform: translateY(14px) scale(1.01); filter: blur(8px); }
}

/* Real bug, found 2026-09-16: the header/nav-dock swap (see "scroll-docked
   icon rail" below) is driven by scroll position, which resets to 0 on
   every fresh page load — but the OLD page's snapshot, taken at the moment
   you click a nav link, still shows whatever scroll state you were
   actually in. Scroll down and dock the icon rail, then click a link, and
   the crossfade above blends the old page's docked icons fading out
   together with the new page's header pills fading in — two different nav
   systems from two different pages, visibly overlapping for the .35s
   crossfade. That's the "fading twice" glitch, and it's a *separate* bug
   from the scroll-triggered docking animation itself (see the nav-dock
   section below for that one's history).
   Fix: pull `.site-header` and `.nav-dock` into their own named
   view-transition groups and give both zero animation — they still get
   replaced by the new page's version, just instantly, with no crossfade to
   blend mismatched states together. The rest of the page (root) keeps the
   normal smooth crossfade. */
.site-header { view-transition-name: site-header; }
.nav-dock { view-transition-name: nav-dock; }
::view-transition-old(site-header),
::view-transition-new(site-header),
::view-transition-old(nav-dock),
::view-transition-new(nav-dock) {
  animation: none;
}

/* dark mode: honors the OS/browser-level preference, whether the user has
   set it permanently or just for this session — no manual toggle needed.
   tokens are fully swapped (not just recolored) so every existing rule
   that uses --ink/--paper/--muted/etc. gets the correct contrast for free. */
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --ink: #f0eee8;
    --paper: #1d1e1b;
    --muted: #a8a8a0;
    --line: rgba(240, 238, 232, .18);
    --accent: #9cb2e1;
    --img-placeholder: #2c2c28;
    /* inverted: light-on-dark contrast block against the now-dark page */
    --accent-surface: #f0eee8;
    --on-accent-surface: #1d1e1b;
    --muted-on-accent: #606058;
    --accent-surface-border: rgba(29, 30, 27, .18);
    --accent-block-border: rgba(29, 30, 27, .5);
    --accent-on-accent: #3964c3;
    --status-error: #e0645a;
    --status-ok: #4cc785;
  }
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; background: var(--paper); overscroll-behavior-y: none; }
body { margin: 0; color: var(--ink); background: var(--paper); font-family: Manrope, Arial, sans-serif; overscroll-behavior-y: none; }
img { display: block; width: 100%; }
a { color: inherit; text-decoration: none; }

/* subtle hover "pop" for every clickable nav/action element site-wide,
   signalling it's a button/link without a heavy or distracting effect */
.wordmark, .header-contact, .text-link, .outline-button {
  transition: transform .18s cubic-bezier(.3, .7, .4, 1.4), color .2s;
}
.wordmark:hover, .header-contact:hover, .text-link:hover, .outline-button:hover {
  transform: scale(1.12);
}

@media (prefers-reduced-motion: no-preference) {
  body.edge-bounce-top { animation: edgeBounceTop .45s cubic-bezier(.34, 1.35, .64, 1); }
  body.edge-bounce-bottom { animation: edgeBounceBottom .45s cubic-bezier(.34, 1.35, .64, 1); }
}

@keyframes edgeBounceTop {
  0% { transform: translateY(0); }
  35% { transform: translateY(16px); }
  100% { transform: translateY(0); }
}
@keyframes edgeBounceBottom {
  0% { transform: translateY(0); }
  35% { transform: translateY(-16px); }
  100% { transform: translateY(0); }
}

body.egg-active { transform: rotate(180deg); transition: transform .6s cubic-bezier(.6, 0, .4, 1); }

.egg-message {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 9999;
  max-width: 86vw;
  padding: 18px 28px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  font: 700 1.1rem 'DM Mono', monospace;
  letter-spacing: .04em;
  text-transform: uppercase;
  text-align: center;
  color: #39ff6a;
  background: rgba(5, 10, 5, .85);
  border: 1px solid rgba(57, 255, 106, .5);
  text-shadow: 0 0 4px #39ff6a, 0 0 14px #39ff6a, 0 0 30px rgba(57, 255, 106, .8);
  animation: eggFlicker 1.8s ease-in-out forwards;
}

.egg-subtitle {
  font-size: .66rem;
  font-weight: 500;
  letter-spacing: .1em;
  opacity: .8;
}

.egg-badge {
  align-self: center;
  margin-top: 2px;
  padding: 3px 11px;
  border: 1px solid rgba(57, 255, 106, .5);
  border-radius: 999px;
  font-size: .56rem;
  letter-spacing: .12em;
}

.egg-log {
  height: 68px;
  margin-top: 2px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 3px;
  text-align: left;
  text-transform: none;
  font-size: .58rem;
  font-weight: 500;
  letter-spacing: .02em;
  opacity: .7;
}
.egg-log div { white-space: nowrap; animation: eggLogIn .2s ease; }

@keyframes eggLogIn {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes eggFlicker {
  0% { opacity: 0; }
  8% { opacity: 1; }
  20% { opacity: .25; }
  23% { opacity: 1; }
  55% { opacity: .3; }
  58% { opacity: 1; }
  100% { opacity: 1; }
}

button { font: inherit; -webkit-appearance: none; appearance: none; border: none; background: none; cursor: pointer; }

/* iOS Safari shows a gray flash box on tap for every link/button by default
   (its built-in "tap highlight") — it clashes with the site's own custom
   pop/pill/color hover-fill feedback on every interactive element, so it's
   turned off globally and left to the site's own animations instead. */
a, button { -webkit-tap-highlight-color: transparent; }

/* --- intro splash ---
   Plain white cover with the wordmark building in letter by letter, held
   for a beat, then a bright flash + outward "blast" reveals the real page.
   Plays once ever per visitor (localStorage, see script.js) — a welcome
   moment, not a loading gate. The separate, always-on version check lives
   in `.update-overlay` below; the two are deliberately different rituals
   (first-visit delight vs. "something changed, hold on").
   `:has()` locks page scroll for exactly as long as either overlay exists
   in the DOM, with no JS needed for that part. */
body:has(.intro-splash), body:has(.update-overlay:not([hidden])) { overflow: hidden; }
.intro-splash {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--paper);
}
.intro-mark {
  display: flex;
  align-items: baseline;
  font-size: clamp(3.5rem, 13vw, 8rem);
  font-weight: 800;
  letter-spacing: -.06em;
  color: var(--ink);
}
.intro-slash { color: var(--acid); margin: 0 .04em; }
.intro-letter { display: inline-block; opacity: 0; transform: scale(.4) translateY(14px); }
@media (prefers-reduced-motion: no-preference) {
  .intro-a { animation: introPop .5s cubic-bezier(.3, .7, .3, 1.4) .1s forwards; }
  .intro-slash { animation: introPop .5s cubic-bezier(.3, .7, .3, 1.4) .3s forwards; }
  .intro-ao { animation: introPop .5s cubic-bezier(.3, .7, .3, 1.4) .5s forwards; }
}
@media (prefers-reduced-motion: reduce) {
  .intro-letter { opacity: 1; transform: none; }
}
@keyframes introPop {
  0% { opacity: 0; transform: scale(.4) translateY(14px); }
  55% { opacity: 1; transform: scale(1.15) translateY(-4px); }
  75% { transform: scale(.95) translateY(1px); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}

/* exit: a quick accent flash, then the mark blasts outward as the whole
   cover fades — the "explode revealing the page" beat */
.intro-splash.intro-exit { pointer-events: none; animation: introFade .55s cubic-bezier(.4, 0, .2, 1) forwards; }
.intro-splash.intro-exit .intro-mark { animation: introBlast .55s cubic-bezier(.3, 0, .2, 1) forwards; }
.intro-splash.intro-exit::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--acid);
  opacity: 0;
  animation: introFlash .3s ease-out forwards;
}
@keyframes introFlash {
  0% { opacity: 0; }
  20% { opacity: .4; }
  100% { opacity: 0; }
}
@keyframes introBlast {
  0% { transform: scale(1); opacity: 1; filter: blur(0); }
  100% { transform: scale(2.3); opacity: 0; filter: blur(6px); }
}
@keyframes introFade {
  0% { opacity: 1; }
  35% { opacity: 1; }
  100% { opacity: 0; }
}

/* --- update overlay ---
   The always-on counterpart to the intro splash: script.js pings
   version.json on every single page load, silently, and this stays
   `hidden` (no paint cost) unless that check actually finds a mismatch —
   most visits never see any of this. When it does fire, a circle wipes
   in from the exact center out past the corners (`clip-path: circle()`,
   which — unlike top/left/right/bottom — animates as a smooth continuous
   shape change, not a snap), the mark appears with a live status line
   underneath, and script.js swaps that line's text as it actually
   fetches the new script.js/styles.css. There's no matching hand-authored
   fade-out: once the fresh assets are warmed in the browser's cache, the
   page reloads outright and lets the site's own `@view-transition` rule
   (top of this file) crossfade FROM whatever this overlay looked like
   straight INTO the refreshed page — reusing that instead of building a
   second exit animation is what makes it "smoothly vanish". */
.update-overlay {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  background: var(--paper);
  clip-path: circle(0% at 50% 50%);
  /* on the base rule (not just `.update-visible`) so removing the class
     plays this in reverse too — closing is meant to be the opening's exact
     mirror (circle wipes back down to nothing, letters retreat), not a
     hard cut to page reload. See script.js's `playUpdateSequence`, which
     waits out this same duration before actually reloading. */
  transition: clip-path .9s cubic-bezier(.4, 0, .2, 1);
}
.update-overlay.update-visible {
  clip-path: circle(150% at 50% 50%);
}
.update-overlay .intro-mark { opacity: 0; transform: translateY(6px); transition: opacity .4s ease .35s, transform .4s ease .35s; }
.update-overlay.update-visible .intro-mark { opacity: 1; transform: translateY(0); }
.update-status {
  min-height: 1.2em;
  margin: 0;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity .4s ease .55s, transform .4s ease .55s;
  font: 500 .72rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--muted);
}
.update-overlay.update-visible .update-status { opacity: 1; transform: translateY(0); }
.update-status.status-error { color: var(--status-error); }
.update-status.status-ok { color: var(--status-ok); }

/* full pop-in -> settle -> wave -> fade-out cycle, looping for as long as
   the overlay stays open (i.e. for as long as the real check/update is
   taking) — one keyframe carries the whole thing, since `animation` is a
   shorthand: giving each phase its own separate animation-name is what
   caused the entrance to silently break earlier (a later rule reassigning
   `animation` replaces the whole thing, including the opacity it was
   holding). The 3 letters share this keyframe with a small stagger so the
   pop-in ripples across them left to right, and (thanks to the same small
   delay) so does the settle/wave/fade — script.js waits for a clean cycle
   boundary (LETTER_CYCLE_MS) before closing, so the ball-wipe never cuts a
   letter off mid-motion; it always closes on the fully-faded frame. */
@keyframes letterCycle {
  0%   { opacity: 0; transform: scale(.4) translateY(14px); }
  10%  { opacity: 1; transform: scale(1.15) translateY(-4px); }
  15%  { transform: scale(.95) translateY(1px); }
  20%  { opacity: 1; transform: scale(1) translateY(0); }
  38%  { transform: scale(1) translateY(-6px); }
  48%  { transform: scale(1) translateY(0); }
  58%  { transform: scale(1) translateY(-6px); }
  68%  { transform: scale(1) translateY(0); }
  82%  { opacity: 0; transform: scale(.6) translateY(-10px); }
  100% { opacity: 0; transform: scale(.6) translateY(-10px); }
}
@media (prefers-reduced-motion: no-preference) {
  .update-overlay.update-visible .intro-letter { animation: letterCycle 3.5s ease-in-out infinite; }
  .update-overlay.update-visible .intro-a { animation-delay: 0s; }
  .update-overlay.update-visible .intro-slash { animation-delay: .12s; }
  .update-overlay.update-visible .intro-ao { animation-delay: .24s; }
}
@media (prefers-reduced-motion: reduce) {
  .update-overlay, .update-overlay .intro-mark, .update-status { transition: opacity .15s linear !important; }
  .update-overlay.update-visible { clip-path: circle(150% at 50% 50%); }
  /* no cycling for reduced-motion — but still needs to actually be
     visible, not just "no animation" (the base .intro-letter rule's resting
     opacity is 0; letterCycle is what normally brings it to 1) */
  .update-overlay .intro-letter { animation: none !important; opacity: 1 !important; transform: none !important; }
}

.site-header { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 28px 4.5vw; border-bottom: 1px solid var(--line); }
/* .brand is the actual grid child now (wordmark + icon row together);
   justify-self:start stops it stretching to fill the grid column — see the
   wordmark's own note below for why that matters in a grid container */
.brand { display: flex; align-items: center; gap: 15px; justify-self: start; }
.wordmark { font-size: 1.25rem; font-weight: 800; letter-spacing: -.1em; transform-origin: left center; }
.wordmark span { color: var(--acid); margin: 0 2px; }

.brand-links { display: flex; align-items: center; gap: 3px; }
.brand-link {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 27px;
  height: 27px;
  color: var(--muted);
  transition: color .3s ease;
}
.brand-link::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--brand-color, var(--acid));
  opacity: 0;
  transform: scale(.4);
  transition: opacity .25s ease, transform .3s cubic-bezier(.3, .7, .3, 1.4);
}
.brand-link svg {
  position: relative;
  z-index: 1;
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linejoin: round;
  stroke-linecap: round;
  transform-origin: center;
}
.brand-link:hover { color: #fff; }
.brand-link:hover::before { opacity: 1; transform: scale(1); }
.brand-link:hover svg { animation: brandPop .6s cubic-bezier(.3, .6, .3, 1); }
.brand-link[data-brand="discord"] { --brand-color: #5865f2; }
.brand-link[data-brand="instagram"] { --brand-color: #d6249f; }
.brand-link[data-brand="tiktok"] { --brand-color: #000000; }
.brand-link[data-brand="whatsapp"] { --brand-color: #25d366; }
.brand-link[data-brand="email"] { --brand-color: var(--acid); }

/* a Discord-reaction-style pop: quick overshoot scale + wiggle, not just a
   flat hover state — same bouncy-easing family as the rest of the site
   (nav hover-pop, star/heart reactions, edge-bounce) */
@keyframes brandPop {
  0% { transform: scale(1) rotate(0deg); }
  30% { transform: scale(1.4) rotate(-14deg); }
  55% { transform: scale(.9) rotate(10deg); }
  75% { transform: scale(1.15) rotate(-5deg); }
  100% { transform: scale(1) rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .brand-link:hover svg { animation: none; }
}

/* easter egg: click the logo 3x fast (see script.js) — no navigation, just
   a five-phase blast: pop up big, shake, explode outward to nothing,
   reappear glitching (hue-shift + skew jitter, cheap but reads as
   "glitch" without a canvas/clip-path rig), then settle back to normal.
   A single normal click still navigates home as usual; only the 3-click
   burst intercepts it. */
.wordmark { position: relative; }
.wordmark.egg-blast { animation: wordmarkEggBlast 2.1s cubic-bezier(.3, .4, .25, 1); transform-origin: center; }
@keyframes wordmarkEggBlast {
  0%   { transform: scale(1) translateY(0) rotate(0deg) skew(0deg); opacity: 1; filter: none; }
  12%  { transform: scale(1.9) translateY(-14px) rotate(0deg); opacity: 1; }
  22%  { transform: scale(1.9) translateY(-14px) rotate(-4deg); }
  30%  { transform: scale(1.9) translateY(-14px) rotate(4deg); }
  38%  { transform: scale(1.9) translateY(-14px) rotate(-3deg); }
  46%  { transform: scale(1.9) translateY(-14px) rotate(2deg); }
  54%  { transform: scale(2.6) translateY(-18px) rotate(0deg); opacity: .6; }
  58%  { transform: scale(3.2) translateY(-20px); opacity: 0; }
  63%  { transform: scale(1) translate(6px, 2px) skew(8deg); opacity: 1; filter: hue-rotate(90deg); }
  67%  { transform: scale(1) translate(-6px, -2px) skew(-8deg); opacity: .7; filter: hue-rotate(-60deg); }
  71%  { transform: scale(1.05) translate(4px, -3px) skew(6deg); opacity: 1; filter: hue-rotate(40deg); }
  75%  { transform: scale(.96) translate(-3px, 3px) skew(-5deg); opacity: .8; filter: none; }
  80%  { transform: scale(1.03) translate(2px, 0) skew(3deg); opacity: 1; }
  85%  { transform: scale(.98) translate(-1px, 1px) skew(-2deg); }
  100% { transform: scale(1) translate(0, 0) rotate(0deg) skew(0deg); opacity: 1; filter: none; }
}

.wordmark-egg-tooltip {
  position: absolute;
  top: calc(100% + 10px);
  left: 50%;
  translate: -50% -6px;
  z-index: 20;
  white-space: nowrap;
  padding: 7px 13px;
  border-radius: 999px;
  font: 600 .64rem 'DM Mono', monospace;
  text-transform: none;
  letter-spacing: .02em;
  color: var(--on-accent-surface);
  background: var(--accent-surface);
  opacity: 0;
  pointer-events: none;
  transition: opacity .22s ease, translate .3s cubic-bezier(.3, .7, .3, 1.4);
}
.wordmark-egg-tooltip.visible { opacity: 1; translate: -50% 0; }
@media (prefers-reduced-motion: reduce) {
  .wordmark.egg-blast { animation: none; }
  .wordmark-egg-tooltip { transition: opacity .15s linear; }
}
.site-nav { display: flex; align-items: center; gap: 8px; font-size: .68rem; text-transform: uppercase; letter-spacing: .14em; }
/* pills never wrap their OWN text internally (that's the ugly failure mode —
   a pill squeezing until "ELISA TAILORING" breaks into two lines inside its
   own rounded border); if the row as a whole doesn't fit, whole pills wrap
   to a second line instead, see the "medium" breakpoint below. */
.site-nav a { white-space: nowrap; }
/* social-button style nav: each item is a standing chip in its own color
   (not flat gray text — it needs to read as a button on its own, at rest),
   then a solid version of that color pops in behind it on hover/focus, the
   same mechanism as .brand-link::before. `z-index: 0` on the link is load-
   bearing: it gives the link its own stacking context so the pseudo's
   `z-index: -1` stays scoped behind THIS link's text instead of escaping
   up and rendering behind the whole page. */
.site-nav a {
  position: relative;
  z-index: 0;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--nav-color, var(--acid)) 38%, var(--line) 62%);
  background: color-mix(in srgb, var(--nav-color, var(--acid)) 11%, transparent);
  color: color-mix(in srgb, var(--nav-color, var(--acid)) 68%, var(--ink) 32%);
  font-weight: 600;
  transition: color .3s ease, border-color .3s ease, background-color .3s ease, opacity .3s ease, transform .35s cubic-bezier(.3, .7, .3, 1);
}
.site-nav a::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: 999px;
  background: var(--nav-color, var(--acid));
  opacity: 0;
  transform: scale(.55);
  transition: opacity .25s ease, transform .3s cubic-bezier(.3, .7, .3, 1.4);
}
.site-nav a:hover, .site-nav a:focus-visible { color: #fff; border-color: transparent; }
.site-nav a:hover::before, .site-nav a:focus-visible::before { opacity: 1; transform: scale(1); }
.site-nav a:hover, .site-nav a:focus-visible { animation: navPop .5s cubic-bezier(.3, .6, .3, 1); }
.site-nav a.active {
  color: var(--ink);
  font-weight: 700;
  border-color: color-mix(in srgb, var(--nav-color, var(--acid)) 55%, var(--line) 45%);
  background: color-mix(in srgb, var(--nav-color, var(--acid)) 18%, transparent);
}
.site-nav a.active:hover, .site-nav a.active:focus-visible { color: #fff; border-color: transparent; }
.site-nav a[data-nav="collection"], .nav-dock a[data-nav="collection"] { --nav-color: #3964c3; }
.site-nav a[data-nav="about"], .nav-dock a[data-nav="about"] { --nav-color: #c9793c; }
.site-nav a[data-nav="market"], .nav-dock a[data-nav="market"] { --nav-color: #8a5cd0; }
.site-nav a[data-nav="archive"], .nav-dock a[data-nav="archive"] { --nav-color: #8a6f52; }
.site-nav a[data-nav="elisa"], .nav-dock a[data-nav="elisa"] { --nav-color: #c9457a; }
.site-nav a[data-nav="policy"], .nav-dock a[data-nav="policy"] { --nav-color: #3f9c8c; }

/* same bouncy overshoot family as brandPop, tuned down (no rotation) since
   this plays on a text label rather than a small icon */
@keyframes navPop {
  0% { transform: scale(1) translateY(0); }
  30% { transform: scale(1.1) translateY(-3px); }
  55% { transform: scale(.96) translateY(1px); }
  75% { transform: scale(1.04) translateY(-1px); }
  100% { transform: scale(1) translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .site-nav a:hover, .site-nav a:focus-visible { animation: none; }
}
.nav-dot { display: inline-block; width: 5px; height: 5px; margin-left: 5px; vertical-align: top; border-radius: 50%; background: var(--nav-color, var(--acid)); }
.site-nav a:hover .nav-dot, .site-nav a:focus-visible .nav-dot { background: #fff; }

/* --- scroll-docked icon rail ---
   Once you scroll past the header, the header pills glide away and this
   icon-only nav glides in to replace them. Two separate elements rather
   than one reshaping itself, because `flex-direction` can't be smoothly
   animated between row and column — it would just snap partway through.

   I tried a fancier @keyframes version first but it caused three separate
   bugs over one afternoon, including a real Chrome rendering glitch, so
   I went back to this simpler plain-transition version instead. Simpler
   also means it can't get caught mid-animation and glitch — it just
   smoothly re-aims wherever it currently is if you scroll back and forth
   fast.

   `scale` and `translate` are kept as separate properties (not one
   `transform`) so each can have its own timing curve — `scale` overshoots
   slightly bigger before settling, which is what makes it feel like it
   "pops in" rather than just shrinking flat. `--stagger` staggers the 6
   icons so they flow in one after another instead of all at once. */
.site-nav a {
  --stagger: 0s;
  scale: 1;
  translate: 0 0;
  transition:
    color .3s ease, border-color .3s ease, background-color .3s ease,
    opacity .35s ease calc(var(--stagger) + .05s),
    scale .4s cubic-bezier(.5, -.6, .2, 1) var(--stagger),
    translate .45s cubic-bezier(.3, .7, .3, 1) calc(var(--stagger) + .08s);
}
.site-nav a:nth-child(1) { --stagger: 0s; }
.site-nav a:nth-child(2) { --stagger: .03s; }
.site-nav a:nth-child(3) { --stagger: .06s; }
.site-nav a:nth-child(4) { --stagger: .09s; }
.site-nav a:nth-child(5) { --stagger: .12s; }
.site-nav a:nth-child(6) { --stagger: .15s; }
body.nav-docked .site-nav a {
  opacity: 0;
  scale: .82;
  translate: 32px 0;
  pointer-events: none;
}

.nav-dock {
  position: fixed;
  top: 50%;
  right: 2.6vw;
  transform: translateY(-50%);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* GHOST ICON BUG (found 2026-09-16) — the pill nav and this dock both
   animate on scroll, each item staggered, but their total times didn't
   match: pills finish by ~.68s, this dock was taking up to ~1.0s. Scroll
   back up fast during that ~.32s gap and the pills/hero are already back
   while a couple of dock icons are still fading out underneath — looks
   like faint ghost icons where nothing should be.
   This is a plain timing mismatch, not a browser bug (that's a different,
   older issue — see [[project_scroll_nav_dock]] history). Fixed by
   trimming this dock's stagger/duration so its worst case (~.71s) lands
   within ~30ms of the pills' (~.68s).
   If either group's timing ever changes again: re-check both worst-case
   totals (last item's stagger + its transition length) still land close
   together, or this exact bug comes back. */
.nav-dock a, .nav-dock .dock-cart-button {
  --stagger: 0s;
  position: relative;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border: 1px solid color-mix(in srgb, var(--nav-color, var(--acid)) 38%, var(--line) 62%);
  border-radius: 50%;
  background: color-mix(in srgb, var(--nav-color, var(--acid)) 11%, var(--paper) 89%);
  color: color-mix(in srgb, var(--nav-color, var(--acid)) 68%, var(--ink) 32%);
  opacity: 0;
  scale: .85;
  translate: 26px 0;
  pointer-events: none;
  transition:
    color .3s ease, border-color .3s ease, background-color .3s ease,
    opacity .4s ease var(--stagger),
    translate .45s cubic-bezier(.22, .8, .3, 1) var(--stagger),
    scale .45s cubic-bezier(.34, 1.8, .3, 1) calc(var(--stagger) + .05s);
}
/* nth-of-type, not nth-child: the cart button (a <button>, not an <a>) is
   only present on 2 of the 6 pages, sitting before these links in the DOM.
   nth-child would count it as sibling #1 and shift every link's stagger
   index by one ONLY on those two pages — nth-of-type counts <a> tags
   among themselves regardless of what else is interspersed, so the same
   1-6 stagger works identically whether or not the button is there. */
.nav-dock a:nth-of-type(1) { --stagger: .06s; }
.nav-dock a:nth-of-type(2) { --stagger: .09s; }
.nav-dock a:nth-of-type(3) { --stagger: .12s; }
.nav-dock a:nth-of-type(4) { --stagger: .15s; }
.nav-dock a:nth-of-type(5) { --stagger: .18s; }
.nav-dock a:nth-of-type(6) { --stagger: .21s; }
.dock-cart-button { --stagger: .03s; --nav-color: #3964c3; }
.nav-dock a::before, .nav-dock .dock-cart-button::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: 50%;
  background: var(--nav-color, var(--acid));
  opacity: 0;
  transform: scale(.55);
  transition: opacity .25s ease, transform .3s cubic-bezier(.3, .7, .3, 1.4);
}
.nav-dock a:hover, .nav-dock a:focus-visible, .nav-dock .dock-cart-button:hover, .nav-dock .dock-cart-button:focus-visible { color: #fff; border-color: transparent; }
.nav-dock a:hover::before, .nav-dock a:focus-visible::before, .nav-dock .dock-cart-button:hover::before, .nav-dock .dock-cart-button:focus-visible::before { opacity: 1; transform: scale(1); }
.nav-dock a:hover, .nav-dock a:focus-visible, .nav-dock .dock-cart-button:hover, .nav-dock .dock-cart-button:focus-visible { animation: navPop .5s cubic-bezier(.3, .6, .3, 1); }
.nav-dock a.active {
  color: var(--ink);
  border-color: color-mix(in srgb, var(--nav-color, var(--acid)) 55%, var(--line) 45%);
  background: color-mix(in srgb, var(--nav-color, var(--acid)) 20%, var(--paper) 80%);
}
.nav-dock a.active:hover, .nav-dock a.active:focus-visible { color: #fff; border-color: transparent; }
.dock-icon { position: relative; z-index: 1; width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linejoin: round; stroke-linecap: round; }
/* same icon set as .dock-icon, reused inline in the header pills so the
   two navs read as one consistent system rather than icon-only vs
   text-only */
.nav-icon { flex-shrink: 0; width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linejoin: round; stroke-linecap: round; }
.nav-dock .nav-dot { position: absolute; top: 7px; right: 7px; margin-left: 0; }
.nav-dock a:hover .nav-dot, .nav-dock a:focus-visible .nav-dot { background: #fff; }

body.nav-docked .nav-dock a, body.nav-docked .nav-dock .dock-cart-button {
  opacity: 1;
  scale: 1;
  translate: 0 0;
  pointer-events: auto;
}

/* the dock shifts left when the cart panel opens so its icons — including
   its own cart button — are never hidden behind the drawer; :has() is
   already used elsewhere in this file (body:has(.intro-splash) etc.), same
   browser-support story applies here. */
.nav-dock {
  transition: transform .35s cubic-bezier(.2, .7, .2, 1);
}
body:has(.cart-panel.visible) .nav-dock {
  transform: translate(-390px, -50%);
}
@media (max-width: 700px) {
  body:has(.cart-panel.visible) .nav-dock { transform: translateY(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .nav-dock { transition: none; }
}

/* the button's name, tucked behind the icon until hover — slides out to the
   left (not just fades) so it reads as the icon "revealing" its label */
.dock-label {
  position: absolute;
  z-index: 1;
  right: calc(100% + 10px);
  top: 50%;
  translate: 10px -50%;
  white-space: nowrap;
  padding: 7px 13px;
  border-radius: 999px;
  font: 600 .64rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .08em;
  background: color-mix(in srgb, var(--nav-color, var(--acid)) 88%, var(--ink) 12%);
  color: #fff;
  opacity: 0;
  pointer-events: none;
  transition: opacity .22s ease, translate .3s cubic-bezier(.3, .7, .3, 1.4);
}
.nav-dock a:hover .dock-label, .nav-dock a:focus-visible .dock-label,
.nav-dock .dock-cart-button:hover .dock-label, .nav-dock .dock-cart-button:focus-visible .dock-label {
  opacity: 1;
  translate: 0 -50%;
}

@media (prefers-reduced-motion: reduce) {
  .site-nav a, .nav-dock a, .nav-dock .dock-cart-button { transition: opacity .15s linear, color .15s linear !important; transform: none !important; transition-delay: 0s !important; }
  .nav-dock a:hover, .nav-dock a:focus-visible, .site-nav a:hover, .site-nav a:focus-visible, .nav-dock .dock-cart-button:hover, .nav-dock .dock-cart-button:focus-visible { animation: none; }
}

@media (max-width: 700px) {
  .nav-dock { right: 3vw; gap: 7px; }
  .nav-dock a, .nav-dock .dock-cart-button { width: 36px; height: 36px; }
  .dock-icon { width: 15px; height: 15px; }
}

.header-contact-group { justify-self: end; display: flex; align-items: center; gap: 22px; }
.header-contact { font-size: .7rem; text-transform: uppercase; letter-spacing: .12em; transform-origin: right center; white-space: nowrap; }
/* keeps the round refresh/cart buttons and "Get in touch" from being squeezed when the header is tight */
.header-contact-group > * { flex-shrink: 0; }
.header-contact span, .text-link span, .outline-button span { margin-left: 8px; }
.header-contact span, .text-link span { color: var(--accent); }
.outline-button span { color: var(--accent-on-accent); }

/* min-height uses vh first (universal fallback), then dvh overrides it where
   supported (iOS Safari 15.4+, all modern browsers) — plain vh on iOS Safari
   is measured as if the address bar were already hidden, so the hero reads
   taller than what's actually visible on first load and then visibly resizes
   once the chrome collapses on scroll; dvh tracks the REAL visible area. */
.hero { min-height: 75vh; min-height: 75dvh; padding: 10vh 4.5vw 5vh; display: flex; flex-direction: column; justify-content: space-between; border-bottom: 1px solid var(--line); }
.hero-kicker, .eyebrow, .image-note { font: .63rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .12em; color: var(--muted); }
.hero-kicker span { color: var(--acid); padding: 0 9px; }
.about-hero h1, .blank-content h1 { margin: 9vh 0 0; font-size: clamp(4.5rem, 11vw, 10.5rem); font-weight: 500; line-height: .87; letter-spacing: -.085em; }
h1 em, h2 em { font-family: var(--serif); font-weight: 400; letter-spacing: -.08em; }
.hero-meta { display: flex; align-items: center; gap: 17px; font: .63rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); }
.scroll-line { display: block; height: 1px; width: 48px; background: var(--ink); }

/* Apple-style "Introducing" hero (homepage only): everything centered, a
   small eyebrow word, the name set huge and tight, then one calm line of
   intro copy. Each line rises in one after the other on load. */
.hero-intro { align-items: center; text-align: center; }
.hero-intro-copy { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.hero-intro-eyebrow { margin: 0; font-size: clamp(1.1rem, 2vw, 1.6rem); font-weight: 600; letter-spacing: -.01em; color: var(--acid); }
.hero-intro-title { margin: .15em 0 0; font-size: clamp(3.4rem, 9.5vw, 8.5rem); font-weight: 800; line-height: .95; letter-spacing: -.055em; }
.hero-intro-lede { max-width: 34ch; margin: 4.5vh 0 0; font-size: clamp(1.05rem, 1.6vw, 1.4rem); font-weight: 500; line-height: 1.45; letter-spacing: -.01em; color: var(--muted); }
.hero-intro .hero-meta { justify-content: center; }

/* The same line-by-line rise greets every page, so arriving anywhere on the
   site feels like one continuous piece. Fill mode is `backwards` on
   purpose: it holds the hidden start state during each delay, then lets go
   completely once finished, so hover effects on these elements (like the
   .text-link pop) keep working afterwards. Market's opening lines wait for
   the loading screen: script.js adds `market-revealed` as it wipes away. */
@media (prefers-reduced-motion: no-preference) {
  .hero-intro-eyebrow, .hero-intro-title, .hero-intro-lede, .hero-intro .hero-meta,
  .about-hero > *, .policy-hero > *, .blank-content > *,
  body.market-revealed .market-page > .section-heading > div > *,
  body.market-revealed .market-page > .section-heading > .section-intro,
  body.market-revealed .market-page > .market-toolbar {
    animation: heroIntroRise 1.1s cubic-bezier(.2, .7, .2, 1) backwards;
  }
  .hero-intro-title,
  .about-hero > :nth-child(2), .policy-hero > :nth-child(2), .blank-content > :nth-child(2),
  body.market-revealed .market-page > .section-heading > div > :nth-child(2) { animation-delay: .15s; }
  .hero-intro-lede,
  .about-hero > :nth-child(3), .policy-hero > :nth-child(3), .blank-content > :nth-child(3),
  body.market-revealed .market-page > .section-heading > .section-intro { animation-delay: .35s; }
  .hero-intro .hero-meta,
  .about-hero > :nth-child(n+4), .policy-hero > :nth-child(n+4), .blank-content > :nth-child(4),
  body.market-revealed .market-page > .market-toolbar { animation-delay: .5s; }
  .blank-content > :nth-child(n+5) { animation-delay: .65s; }
}
@keyframes heroIntroRise {
  from { opacity: 0; transform: translateY(24px); }
  to { opacity: 1; transform: none; }
}

/* one continuous horizontal strip, sliced by parallel diagonal cuts running
   through every photo together — a clip-path parallelogram per cell (not a
   skewX transform) so the cut edges stay pixel-crisp and the photos
   themselves are never distorted. --cut controls the slant amount; each
   cell overlaps the previous by exactly --cut so the diagonal edges meet
   with no seam or gap. */
/* A REAL horizontal scroll container, not a transform-animated fake one —
   script.js drives a slow, stepped auto-scroll (slide to the end, HOLD,
   slide back, HOLD, repeat) when idle, and it's genuine `overflow-x: auto`
   underneath so a visitor's own swipe/drag/wheel scroll always works and
   simply takes over; see the "highlights strip" section of script.js for
   the pause-on-interaction / resume-after-idle logic. Scrollbar is hidden
   since the auto-scroll and the diagonal-cut edges already read as the
   strip's own UI — a visible native scrollbar under that would look like a
   stray browser chrome element, not part of the design. */
.highlights-strip {
  --cut: clamp(14px, 3vw, 56px);
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  will-change: scroll-position;
  border-bottom: 1px solid var(--line);
  background: var(--img-placeholder);
}
.highlights-strip::-webkit-scrollbar { display: none; }
.highlights-row {
  display: flex;
  width: max-content;
  height: clamp(220px, 32vw, 420px);
}
.highlight-cell {
  position: relative;
  flex: 0 0 clamp(220px, 30vw, 480px);
  height: 100%;
  margin-left: calc(-1 * var(--cut));
  clip-path: polygon(var(--cut) 0, 100% 0, calc(100% - var(--cut)) 100%, 0 100%);
  /* clip-path masking + scroll is a known Safari/WebKit perf trap — without
     a GPU layer hint, it can re-rasterize each cell's diagonal cut in
     software on every scroll frame instead of just moving a pre-rendered
     layer, which reads as laggy/stuttery scrolling on mobile. Forcing each
     cell onto its own compositor layer fixes that. */
  will-change: transform;
  transform: translateZ(0);
}
.highlight-cell:first-child {
  margin-left: 0;
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--cut)) 100%, 0 100%);
}
.highlight-cell:last-child {
  clip-path: polygon(var(--cut) 0, 100% 0, 100% 100%, 0 100%);
}
.highlight-cell a { display: block; width: 100%; height: 100%; }
.highlight-cell img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  --fog-blur: 18px;
  filter: blur(var(--fog-blur));
  transition: transform .6s cubic-bezier(.2,.7,.2,1), filter 1.1s cubic-bezier(.2, .6, .2, 1);
}
.highlight-cell:hover img { transform: scale(1.06); }

.collection-section, .market-page { padding: 13vh 4.5vw; }
.section-heading { display: flex; justify-content: space-between; align-items: end; margin-bottom: 8vh; }
h2 { margin: 18px 0 0; font-size: clamp(3rem, 6.5vw, 7rem); line-height: .9; font-weight: 500; letter-spacing: -.08em; }
.section-intro { width: 245px; margin: 0 8% 4px 0; font-size: .8rem; line-height: 1.7; color: var(--muted); }
.gallery-grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 5vw 1.7vw; align-items: start; }
.gallery-card .image-wrap { position: relative; overflow: hidden; background: var(--img-placeholder); }
.gallery-card img {
  height: 100%;
  object-fit: cover;
  --fog-blur: 18px;
  filter: blur(var(--fog-blur));
  transition: transform .7s cubic-bezier(.2,.7,.2,1), filter 1.1s cubic-bezier(.2, .6, .2, 1);
}

/* light friction against casual "save image" attempts on the actual print
   photography (not real protection — a screenshot always gets through —
   just enough to stop right-click/long-press saving the raw file directly.
   see the low-res-preview strategy for what actually deters copying. */
.gallery-card img,
.story-image img {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
  user-drag: none;
}
.gallery-card:hover img { transform: scale(1.05); }

/* fog reveal: every content photo starts blurred and resolves into focus
   the moment it scrolls into view (see script.js — an IntersectionObserver,
   not manual scroll-position math, which is exactly what caused the
   nav-dock's three separate scroll-animation bugs earlier). --fog-blur is
   set per image-type above (so each keeps its own base filter, e.g.
   .story-image's saturate) and just gets zeroed out here on reveal. */
.gallery-card img.fog-revealed, .highlight-cell img.fog-revealed, .story-image img.fog-revealed {
  --fog-blur: 0px;
}
@media (prefers-reduced-motion: reduce) {
  .gallery-card img, .highlight-cell img, .story-image img { --fog-blur: 0px !important; transition: none; }
}
.gallery-card .image-wrap.has-loupe:hover { cursor: none; }
.gallery-card .image-wrap.has-loupe.near-buy:hover { cursor: auto; }

.loupe {
  position: absolute;
  top: 0;
  left: 0;
  width: 190px;
  height: 190px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .75);
  box-shadow: 0 12px 40px rgba(0, 0, 0, .4), inset 0 0 0 1px rgba(0, 0, 0, .06);
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -50%) scale(.85);
  transition: opacity .2s ease, transform .2s ease;
  background-repeat: no-repeat;
  z-index: 6;
}
.loupe.active { opacity: 1; transform: translate(-50%, -50%) scale(1); }
/* arrival flourish: played once, when animatedScrollTo() (script.js) lands
   on a card reached via an in-page link — a ring that ripples outward from
   the card and a small settle-bounce, so the destination is unmistakable.
   Lives on .gallery-card itself, not .image-wrap — .image-wrap has
   overflow:hidden, which would clip the ring's outward glow to nothing. */
.gallery-card.arrival-pulse { animation: arrivalRing 1.1s cubic-bezier(.2, .7, .2, 1), arrivalBounce 1.1s cubic-bezier(.2, .7, .2, 1); }
@keyframes arrivalRing {
  0% { box-shadow: 0 0 0 0 rgba(57, 100, 195, 0); }
  18% { box-shadow: 0 0 0 8px rgba(57, 100, 195, .5); }
  100% { box-shadow: 0 0 0 34px rgba(57, 100, 195, 0); }
}
@keyframes arrivalBounce {
  0%, 100% { transform: scale(1); }
  18% { transform: scale(1.025); }
  40% { transform: scale(.995); }
  60% { transform: scale(1.008); }
}
@media (prefers-reduced-motion: reduce) {
  .gallery-card.arrival-pulse { animation: none; }
}

.card-large { grid-column: 1 / 8; }
/* was 1.25 (landscape) — every current print is portrait-orientation, so a
   landscape box was cropping most of the frame away; .9 keeps the slot's
   feature-sized footprint without butchering the composition */
.card-large .image-wrap { aspect-ratio: .9; }
.card-portrait { grid-column: 9 / 13; margin-top: 9vw; }
.card-portrait .image-wrap { aspect-ratio: .78; }
.card-wide { grid-column: 3 / 11; }
/* was 1.8 (panoramic) — same issue, more so: a portrait photo in a 1.8 box
   was left showing a thin center sliver. 1.15 is still the "wide" slot
   relative to its neighbors, just not fighting the source material */
.card-wide .image-wrap { aspect-ratio: 1.15; object-position: 50% 35%; }
.card-square { grid-column: 1 / 6; margin-top: 5vw; }
.card-square .image-wrap { aspect-ratio: 1; }
.card-tall { grid-column: 8 / 13; margin-top: 15vw; }
.card-tall .image-wrap { aspect-ratio: .84; }

/* market grid: a plain uniform catalog layout (unlike the hand-placed
   .gallery-grid bento on the Collection page) since it needs to scale to
   dozens of items without hand-tuning grid-column spans for each one.
   Reuses .gallery-card/.image-wrap/.buy-button/.reactions as-is — those are
   generic components already, not tied to the bento positioning classes. */
.market-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 4vw 1.4vw; margin-top: 2vh; }
.market-grid .image-wrap { aspect-ratio: .8; }
@media (max-width: 700px) {
  .market-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9vw 3vw; }
}

/* pagination: only page 1's cards are visible by default (this rule alone
   is what stops the other ~33 full-res images from ever being requested —
   `loading="lazy"` images never fetch while `display:none`), script.js just
   toggles which page's cards carry this attribute-mismatch. Kept as a plain
   CSS default (not JS-applied on load) so page 1 is still correct output
   for a no-JS visitor, matching this site's existing no-JS stance elsewhere. */
.market-grid .gallery-card[data-page]:not([data-page="1"]) { display: none; }
.market-pagination { display: flex; align-items: center; justify-content: center; gap: 3vw; margin-top: 8vh; font: 600 .7rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .1em; }
.market-pagination button { border: 0; background: none; padding: 10px; min-width: 40px; min-height: 40px; cursor: pointer; color: var(--ink); }
.market-pagination button:disabled { color: var(--muted); opacity: .4; cursor: default; }
.page-numbers { display: flex; gap: 1.6vw; }
.page-numbers button { min-width: 1.6em; border-radius: 50%; color: var(--muted); }
.page-numbers button.active { color: var(--ink); background: var(--img-placeholder); }
.page-numbers button:hover:not(.active) { color: var(--ink); }

/* --- market toolbar: fast edition filters + smart content search ---
   Filter pills reuse the same color-per-state idea as .edition-badge
   (blue/red/green for open/limited/free) so a visitor already reading
   badge colors on the cards recognizes the same language up here. */
.market-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 3vw; flex-wrap: wrap; margin-top: 4vh; }

.market-filters { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.market-filter {
  padding: 7px 16px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--filter-color, var(--acid)) 38%, var(--line) 62%);
  background: color-mix(in srgb, var(--filter-color, var(--acid)) 11%, transparent);
  color: color-mix(in srgb, var(--filter-color, var(--acid)) 68%, var(--ink) 32%);
  font: 700 .68rem 'Manrope', inherit;
  text-transform: uppercase;
  letter-spacing: .12em;
  cursor: pointer;
  transition: color .3s ease, border-color .3s ease, background-color .3s ease, transform .2s ease;
}
.market-filter:hover, .market-filter:focus-visible { transform: scale(1.06); }
.market-filter.active { color: #fff; border-color: transparent; background: var(--filter-color, var(--acid)); }
.market-filter[data-filter="limited"] { --filter-color: var(--status-error); }
.market-filter[data-filter="free"] { --filter-color: var(--status-ok); }

/* Smart search: a plain circular lens (matches .refresh-check's size/border
   language) that expands into a monospace search bar — DM Mono to match the
   site's existing terminal/DOS easter-egg idiom elsewhere. The blinking
   caret is a hand-built span, not the input's native one: it needs to show
   the instant the field opens, before the input is even focused, and step
   aside for the real caret the moment there's text. */
.market-search { position: relative; display: flex; align-items: center; height: 30px; }
.market-search-toggle {
  position: relative; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; flex: none;
  padding: 0; border: 1px solid var(--line); border-radius: 50%;
  background: var(--paper); color: var(--muted); cursor: pointer;
  transition: color .2s ease, border-color .2s ease, box-shadow .25s ease;
}
/* More wound-up than the standard .navPop pill hover — this is the one
   control on the page whose whole job is "click me", so its hover gets its
   own louder wiggle+glow rather than reusing the shared pop. */
.market-search-toggle:hover, .market-search-toggle:focus-visible {
  color: var(--ink); border-color: var(--ink);
  animation: marketSearchHype .6s cubic-bezier(.3, .6, .3, 1);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--acid) 22%, transparent);
}
@keyframes marketSearchHype {
  0% { transform: scale(1) rotate(0deg); }
  20% { transform: scale(1.3) rotate(-14deg); }
  40% { transform: scale(.92) rotate(10deg); }
  60% { transform: scale(1.16) rotate(-6deg); }
  80% { transform: scale(.98) rotate(2deg); }
  100% { transform: scale(1) rotate(0deg); }
}
.market-search[data-state="open"] .market-search-toggle { color: var(--ink); border-color: var(--ink); }
.market-search-icon { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
.market-search-field {
  display: flex; align-items: center;
  width: 0; margin-left: 0; overflow: hidden;
  border: 1px solid transparent; border-radius: 999px;
  background: var(--paper);
  transition: width .35s cubic-bezier(.3, .7, .3, 1), margin-left .35s cubic-bezier(.3, .7, .3, 1), border-color .2s ease;
}
.market-search[data-state="open"] .market-search-field { width: 220px; margin-left: -15px; padding-left: 15px; border-color: var(--line); }
.market-search-input {
  flex: 1; min-width: 0; border: 0; background: none; color: var(--ink);
  font: 500 .75rem 'DM Mono', monospace; padding: 8px 4px 8px 0;
  outline: none;
}
.market-search-input::placeholder { color: var(--muted); }
.market-search-caret {
  flex: none; width: 7px; height: 2px; margin-right: 6px; background: var(--ink);
  opacity: 0; animation: marketCaretBlink 1s step-end infinite;
}
.market-search[data-state="open"]:not(.has-value) .market-search-caret { opacity: 1; }
.market-search-clear {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; margin-right: 6px; padding: 0;
  border: 0; background: none; color: var(--muted); cursor: pointer;
  font-size: .65rem; opacity: 0; transition: opacity .2s ease, color .2s ease;
}
.market-search.has-value .market-search-clear { opacity: 1; }
.market-search-clear:hover { color: var(--ink); }
@keyframes marketCaretBlink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

/* Search-in-progress "sweep": cards dip out, matches pop back in staggered
   (--reveal-stagger set per-card by script.js), so a search always feels
   like it did something even though the actual filter is instant. */
.market-grid.market-searching .gallery-card { transition: opacity .2s ease, scale .2s ease; opacity: 0; scale: .96; pointer-events: none; }
.market-grid.market-revealing .gallery-card {
  animation: marketCardIn .4s cubic-bezier(.2, .7, .2, 1) backwards;
  animation-delay: var(--reveal-stagger, 0s);
}
@keyframes marketCardIn { from { opacity: 0; scale: .94; translate: 0 10px; } to { opacity: 1; scale: 1; translate: 0 0; } }

.market-empty { text-align: center; padding: 8vh 4vw; color: var(--muted); font-size: .95rem; }
.market-empty span { color: var(--ink); font-weight: 600; }
.market-empty button { color: var(--ink); text-decoration: underline; background: none; border: 0; cursor: pointer; font: inherit; padding: 0; }

@media (prefers-reduced-motion: reduce) {
  .market-search-caret { animation: none; opacity: 1; }
  .market-search-field, .market-search-toggle, .market-filter { transition: none; }
  .market-search-toggle:hover, .market-search-toggle:focus-visible { animation: none; box-shadow: none; }
  .market-grid.market-searching .gallery-card, .market-grid.market-revealing .gallery-card { animation: none; transition: none; opacity: 1; scale: 1; }
}

@media (max-width: 700px) {
  .market-toolbar { flex-direction: column; align-items: stretch; gap: 14px; }
  .market-filters { justify-content: center; }
  .market-search { justify-content: flex-end; }
  .market-search[data-state="open"] .market-search-field { width: min(220px, 62vw); }
}

.buy-button { position: absolute; right: 14px; bottom: 14px; padding: 10px 13px; border: 0; color: var(--on-acid); background: var(--acid); font-size: .66rem; text-transform: uppercase; letter-spacing: .1em; cursor: pointer; opacity: 0; transform: translateY(8px); transition: opacity .25s, transform .25s; transform-origin: bottom right; }
.gallery-card:hover .buy-button, .buy-button:focus { opacity: 1; transform: translateY(0); }
.gallery-card:hover .buy-button:hover, .buy-button:focus:hover { transform: translateY(0) scale(1.12); }

/* label <-> price dissolve: label sits in normal flow (so it sets the
   button's width), price is stacked on top via absolute positioning and
   crossfades in — on hover, and periodically on an idle timer (see
   scheduleIdleFlash in script.js) so the price surfaces even without a
   pointer nearby. Both transitions are slow/soft on purpose ("dissolve",
   not a snap) — a blur pass alongside the opacity fade sells that. */
.buy-button .buy-label { display: flex; align-items: center; gap: 6px; transition: opacity .7s ease, filter .7s ease; }
.buy-button .buy-price {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  filter: blur(3px);
  transition: opacity .7s ease, filter .7s ease;
  pointer-events: none;
}
.buy-button:hover .buy-label,
.buy-button.reveal-price .buy-label { opacity: 0; filter: blur(3px); }
.buy-button:hover .buy-price,
.buy-button.reveal-price .buy-price { opacity: 1; filter: blur(0); }
@media (prefers-reduced-motion: reduce) {
  .buy-button .buy-label, .buy-button .buy-price { transition: opacity .2s linear; filter: none !important; }
}

/* reactions: hidden until hover (visible always on touch, via the mobile
   override below), but a button that's already active stays visible so a
   returning visitor can see their own star/heart at a glance without hovering */
.reactions { position: absolute; bottom: 14px; left: 14px; z-index: 6; display: flex; gap: 8px; opacity: 0; transform: translateY(8px); transition: opacity .25s, transform .25s; }
.gallery-card:hover .reactions, .reactions:has(.active) { opacity: 1; transform: translateY(0); }
.react-btn { display: flex; align-items: center; gap: 5px; border: 0; padding: 7px 10px; border-radius: 999px; background: rgba(15, 15, 12, .45); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); color: #fff; font: 500 .62rem 'DM Mono', monospace; cursor: pointer; }
.react-icon { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linejoin: round; transform-origin: center; transition: transform .3s ease, fill .3s ease, stroke .3s ease, filter .3s ease; }
.react-count { min-width: 1ch; }

/* .active = settled resting look (restored on page load, no animation).
   .pop is added only at the moment of an actual click, to play the
   glow/vibrate/pulse keyframes once; it removes itself on animationend. */
.react-star.active .react-icon { fill: #ffcf40; stroke: #ffcf40; filter: drop-shadow(0 0 4px rgba(255, 207, 64, .65)); transform: scale(1.1); }
.react-heart.active .react-icon { fill: #ff4d6d; stroke: #ff4d6d; transform: scale(1.15); }
.react-star.pop .react-icon { animation: starReact .9s cubic-bezier(.3, .6, .3, 1); }
.react-heart.pop .react-icon { animation: heartReact .7s cubic-bezier(.3, .6, .3, 1); }

@keyframes starReact {
  0% { transform: scale(1) rotate(0deg); }
  10% { transform: scale(.85) rotate(-6deg); }
  22% { transform: scale(1.2) rotate(6deg); }
  34% { transform: scale(.95) rotate(-4deg); }
  46% { transform: scale(1.22) rotate(4deg); }
  58% { transform: scale(1) rotate(-2deg); }
  70% { transform: scale(1.12) rotate(2deg); }
  85% { transform: scale(1.02) rotate(-1deg); }
  100% { transform: scale(1.1) rotate(0deg); }
}
@keyframes heartReact {
  0% { transform: scale(1); }
  25% { transform: scale(1.5); }
  45% { transform: scale(1.15); }
  65% { transform: scale(1.32); }
  85% { transform: scale(1.05); }
  100% { transform: scale(1.15); }
}

/* edition badge: a small shield dropping down from the top-right corner of
   every gallery card — blue for open edition (the default, can be bought
   any number of times), red for limited edition (one-of-one, retired from
   sale the moment it's bought), green for free. Color/text are driven by
   script.js reading data-edition (+ the buy-button's price, for free) off
   the card, not hardcoded per-card, so there's exactly one source of truth
   per card to update rather than two things that could drift out of sync. */
.edition-badge {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 5px 10px 5px 7px;
  border: 0;
  border-radius: 999px;
  font: 700 .6rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: #fff;
  background: var(--edition-color, var(--accent));
  cursor: pointer;
  transition: transform .2s cubic-bezier(.3, .7, .3, 1.4);
}
.edition-badge:hover, .edition-badge:focus-visible { transform: scale(1.08); }
.edition-badge svg { width: 12px; height: 12px; fill: currentColor; stroke: none; }
/* keyed off the badge's OWN data-state (set by script.js from the card's
   true data-edition PLUS whether the price is 0), not the card's
   data-edition directly — free is a price fact, not an edition fact, so a
   free LIMITED piece is still limited (retired after one sale, etc.) even
   though its badge displays as green "Free" rather than red "Limited". */
.edition-badge[data-state="open"] { --edition-color: #3964c3; }
.edition-badge[data-state="limited"] { --edition-color: #c0392b; }
.edition-badge[data-state="free"] { --edition-color: #2f9e5b; }
@media (prefers-reduced-motion: no-preference) {
  .edition-badge { animation: editionBadgeDrop .6s cubic-bezier(.3, .7, .3, 1.3) .3s backwards; }
}
@keyframes editionBadgeDrop {
  0% { transform: translateY(-140%); opacity: 0; }
  60% { transform: translateY(8%); opacity: 1; }
  80% { transform: translateY(-3%); }
  100% { transform: translateY(0); opacity: 1; }
}

/* one shared tooltip element, repositioned by script.js above whichever
   badge is hovered/focused, rather than a copy inside every one of the
   102 badges across market.html + index.html. */
.edition-tooltip {
  position: fixed;
  z-index: 40;
  max-width: 240px;
  padding: 9px 14px;
  border-radius: 12px;
  font: 600 .68rem/1.4 'DM Mono', monospace;
  text-transform: none;
  letter-spacing: normal;
  color: var(--on-accent-surface);
  background: var(--accent-surface);
  box-shadow: 0 10px 26px rgba(0, 0, 0, .18);
  opacity: 0;
  translate: -50% 6px;
  pointer-events: none;
  transition: opacity .18s ease, translate .22s cubic-bezier(.3, .7, .3, 1.4);
}
.edition-tooltip.visible { opacity: 1; translate: -50% 0; }
@media (prefers-reduced-motion: reduce) {
  .edition-tooltip { transition: opacity .12s linear; }
}
@media (prefers-reduced-motion: reduce) {
  .react-star.active .react-icon, .react-heart.active .react-icon { animation: none; transform: scale(1.1); }
}
.card-caption { display: flex; justify-content: space-between; gap: 10px; padding-top: 11px; color: var(--muted); font: .6rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .08em; }
.manifesto { padding: 16vh 20% 18vh; border-top: 1px solid var(--line); text-align: center; }
.manifesto p { max-width: 680px; margin: 30px auto 35px; font-size: clamp(1.8rem, 3.4vw, 3.5rem); line-height: 1.04; letter-spacing: -.07em; }
.text-link { display: inline-block; padding-bottom: 7px; border-bottom: 1px solid var(--ink); font: .68rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .1em; }

.about-hero { padding: 10vh 4.5vw 13vh; border-bottom: 1px solid var(--line); }
.about-hero h1 { margin-top: 12vh; }
.about-lede { width: min(410px, 48%); margin: 5vh 0 0 52%; font-size: 1.05rem; line-height: 1.6; }
.about-story { display: grid; grid-template-columns: 5fr 4fr; gap: 10vw; align-items: center; padding: 14vh 10vw; }
.story-image-frame { position: relative; }
.story-image img {
  aspect-ratio: .8;
  object-fit: cover;
  --fog-blur: 18px;
  filter: blur(var(--fog-blur)) saturate(.75);
  transition: filter 1.1s cubic-bezier(.2, .6, .2, 1);
}
.image-note { position: absolute; right: 12px; bottom: 12px; color: white; }
.image-credit { display: block; margin-top: 10px; font: .63rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .12em; color: var(--muted); }
.story-copy { max-width: 430px; }
.large-copy { margin: 27px 0; font-size: 1.6rem; line-height: 1.15; letter-spacing: -.05em; }
.story-copy > p:not(.large-copy) { color: var(--muted); font-size: .82rem; line-height: 1.8; margin-bottom: 30px; }
.specs-section { display: grid; grid-template-columns: 1fr 1fr; gap: 10vw; padding: 14vh 10vw; border-top: 1px solid var(--line); }
.elisa-section { display: grid; grid-template-columns: 1fr 1fr; gap: 10vw; padding: 14vh 10vw; border-top: 1px solid var(--line); align-items: center; }
.spec-list { border-top: 1px solid var(--line); }
.spec-row { display: flex; justify-content: space-between; gap: 25px; padding: 19px 0; border-bottom: 1px solid var(--line); font-size: .75rem; }
.spec-row span { color: var(--muted); }
.spec-row strong { text-align: right; font-weight: 500; }
/* Was a permanent "accent surface" (always dark-on-light or light-on-dark,
   contrasting against the page around it — see --accent-surface, still
   used that way by .toast/.refresh-tooltip, which genuinely benefit from
   always standing out as transient overlays). That was the wrong call for
   a full page SECTION like this one, which every other section on the
   page (about-hero, about-story, elisa-section...) just renders as normal
   page content — user reported it repeatedly as "stuck black in light
   mode" because that's exactly what it was doing by design. Fixed: plain
   --paper/--ink like the rest of the page, no special accent treatment. */
.about-cta { padding: 15vh 4.5vw; background: var(--paper); color: var(--ink); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.about-cta h2 { margin: 40px 0 55px; }
.outline-button { display: inline-block; padding: 15px 18px; border: 1px solid var(--accent-block-border); font: .68rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .1em; }
.blank-content { min-height: calc(100vh - 160px); min-height: calc(100dvh - 160px); padding: 13vh 4.5vw; }
.blank-content h1 { margin-top: 15vh; }
.blank-content .text-link { margin-top: 8vh; }
.site-footer { display: flex; justify-content: space-between; padding: 25px 4.5vw; border-top: 1px solid var(--line); color: var(--muted); font: .68rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .08em; }

/* manual "force a check" button — sits left of the email, with real gap
   between it and the mailto link so it doesn't read as part of the address.
   Triggers the exact same overlay sequence as a genuine version mismatch
   (see script.js) so a visitor who's stuck sees real, complete feedback
   rather than nothing happening. */
.footer-right { display: flex; align-items: center; gap: 18px; }
.refresh-check {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color .2s ease, border-color .2s ease, transform .2s ease;
}
.refresh-check:hover, .refresh-check:focus-visible { color: var(--ink); border-color: var(--ink); }
.refresh-icon { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; transition: transform .2s ease; }
.refresh-check:hover .refresh-icon, .refresh-check:focus-visible .refresh-icon { transform: rotate(-40deg); }
.refresh-tooltip {
  position: absolute;
  bottom: calc(100% + 10px);
  right: 50%;
  translate: 50% 6px;
  white-space: nowrap;
  max-width: calc(100vw - 24px);
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 7px 13px;
  border-radius: 999px;
  font: 600 .64rem 'DM Mono', monospace;
  text-transform: none;
  letter-spacing: .02em;
  background: var(--accent-surface);
  color: var(--on-accent-surface);
  opacity: 0;
  pointer-events: none;
  transition: opacity .22s ease, translate .3s cubic-bezier(.3, .7, .3, 1.4);
}
.refresh-check:hover .refresh-tooltip, .refresh-check:focus-visible .refresh-tooltip { opacity: 1; translate: 50% 0; }
/* the header copy of this button sits at the very top of the viewport —
   popping the tooltip upward (the footer version's direction) would run
   off-screen, so this variant flips it to open downward instead */
.refresh-check--down .refresh-tooltip { bottom: auto; top: calc(100% + 10px); translate: 50% -6px; }
.refresh-check--down:hover .refresh-tooltip, .refresh-check--down:focus-visible .refresh-tooltip { translate: 50% 0; }
@media (prefers-reduced-motion: reduce) {
  .refresh-check:hover .refresh-icon, .refresh-check:focus-visible .refresh-icon { transform: none; }
}
.toast { position: fixed; right: 24px; bottom: 24px; z-index: 5; max-width: 290px; padding: 15px 18px; color: var(--on-accent-surface); background: var(--accent-surface); font-size: .75rem; line-height: 1.5; opacity: 0; transform: translateY(15px); transition: opacity .25s, transform .25s; pointer-events: none; }
.toast.visible { opacity: 1; transform: translateY(0); }

/* bottom-centered "back to top" button — plain fade + rise, shown once
   you've scrolled roughly a viewport down (see script.js for the
   threshold/hysteresis). Kept deliberately simple (no bounce/keyframes),
   matching this site's existing preference for plain transitions over
   fancier animation elsewhere. */
.scroll-top-btn {
  position: fixed;
  left: 50%;
  bottom: 28px;
  translate: -50% 14px;
  z-index: 55;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--paper);
  color: var(--ink);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  box-shadow: 0 4px 18px rgba(0, 0, 0, .12);
  transition: opacity .3s ease, translate .3s cubic-bezier(.3, .7, .3, 1), border-color .2s ease;
}
.scroll-top-btn.visible { opacity: 1; translate: -50% 0; pointer-events: auto; }
.scroll-top-btn:hover, .scroll-top-btn:focus-visible { border-color: var(--ink); }
.scroll-top-btn svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
@media (prefers-reduced-motion: reduce) {
  .scroll-top-btn { transition: opacity .3s ease; }
}

/* digital-download consent modal — gates every purchase behind the express
   waiver required by the Consumer Contracts Regulations 2013 (see
   policy.html). [hidden] handles true removal (a11y + no stray tab stops);
   .visible drives the actual fade/scale-in, toggled a frame after [hidden]
   is cleared so the transition has something to animate from. */
.consent-modal { position: fixed; inset: 0; z-index: 70; /* above the cart panel (52), which stays open behind it */ display: flex; align-items: center; justify-content: center; padding: 20px; }
.consent-modal[hidden] { display: none; }
.consent-modal-backdrop { position: absolute; inset: 0; background: rgba(15, 15, 12, .55); opacity: 0; transition: opacity .3s ease; }
.consent-modal.visible .consent-modal-backdrop { opacity: 1; }
.consent-modal-box {
  position: relative;
  z-index: 1;
  width: min(480px, 100%);
  max-height: 85vh;
  max-height: 85dvh;
  overflow-y: auto;
  padding: 34px 30px 28px;
  background: var(--paper);
  border: 1px solid var(--line);
  opacity: 0;
  transform: translateY(18px) scale(.97);
  transition: opacity .35s cubic-bezier(.2, .7, .2, 1), transform .35s cubic-bezier(.2, .7, .2, 1);
}
.consent-modal.visible .consent-modal-box { opacity: 1; transform: translateY(0) scale(1); }
.consent-modal-close { position: absolute; top: 14px; right: 14px; border: 0; background: none; font-size: .95rem; color: var(--muted); cursor: pointer; line-height: 1; padding: 8px; transition: color .2s, transform .2s; }
.consent-modal-close:hover { color: var(--ink); transform: scale(1.15); }
.consent-modal-box h3 { margin: 10px 0 18px; font-size: 1.5rem; font-weight: 500; letter-spacing: -.02em; }
.consent-modal-box .consent-price { color: var(--accent); }
.consent-copy { font-size: .85rem; line-height: 1.75; color: var(--muted); margin: 0 0 22px; }
.consent-copy strong { color: var(--ink); font-weight: 600; }
.consent-name-note { font-size: .78rem; line-height: 1.6; color: var(--muted); margin: 0 0 18px; }
.consent-checkbox { display: flex; align-items: flex-start; gap: 10px; font-size: .78rem; line-height: 1.6; color: var(--ink); cursor: pointer; margin-bottom: 24px; }
.consent-checkbox input { margin-top: 3px; width: 16px; height: 16px; accent-color: var(--acid); flex-shrink: 0; cursor: pointer; }
.consent-modal-actions { display: flex; gap: 12px; justify-content: flex-end; align-items: center; }
.consent-cancel { border: 1px solid var(--line); background: none; padding: 12px 16px; font: .68rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); cursor: pointer; transition: color .2s, border-color .2s; }
.consent-cancel:hover { color: var(--ink); border-color: var(--ink); }
.consent-confirm { border: 0; padding: 12px 18px; font: .68rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .1em; color: var(--on-acid); background: var(--acid); cursor: pointer; display: flex; align-items: center; gap: 8px; transition: opacity .2s, transform .2s; }
.consent-confirm:hover:not(:disabled) { transform: scale(1.05); }
.consent-confirm:disabled { opacity: .35; cursor: not-allowed; }
.order-reference { display: flex; align-items: center; gap: 10px; margin: 0 0 24px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 6px; background: color-mix(in srgb, var(--acid) 6%, transparent); }
.order-reference code { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; font: 500 .75rem 'DM Mono', monospace; color: var(--ink); }
.order-reference-copy { flex-shrink: 0; border: 1px solid var(--line); background: none; padding: 7px 12px; font: .62rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); cursor: pointer; transition: color .2s, border-color .2s; }
.order-reference-copy:hover { color: var(--ink); border-color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
  .consent-modal-backdrop, .consent-modal-box { transition: opacity .15s linear; transform: none !important; }
}

/* enlarge button: top-left of every card's image, mirroring the edition
   badge's top-right corner. Opens the full-res watermarked "sample" —
   safe to show at full size precisely because the watermark is baked in. */
.enlarge-button {
  position: absolute;
  top: 14px;
  left: 14px;
  z-index: 6;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 999px;
  color: #fff;
  background: rgba(15, 15, 12, .45);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  cursor: pointer;
  transition: transform .2s cubic-bezier(.3, .7, .3, 1.4), background .2s ease;
}
.enlarge-button:hover, .enlarge-button:focus-visible { transform: scale(1.1); background: rgba(15, 15, 12, .65); }
.enlarge-button svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

.image-lightbox { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 26px; }
.image-lightbox[hidden] { display: none; }
.image-lightbox-backdrop { position: absolute; inset: 0; background: rgba(10, 10, 8, .82); opacity: 0; transition: opacity .3s ease; }
.image-lightbox.visible .image-lightbox-backdrop { opacity: 1; }
.image-lightbox-box { position: relative; z-index: 1; max-width: min(1100px, 100%); max-height: 90vh; max-height: 90dvh; opacity: 0; transform: scale(.97); transition: opacity .3s cubic-bezier(.2, .7, .2, 1), transform .3s cubic-bezier(.2, .7, .2, 1); }
.image-lightbox.visible .image-lightbox-box { opacity: 1; transform: scale(1); }
/* The box above is just the frame. The image itself gets its own, slightly
   longer fade+bloom, only triggered once decode() confirms a real frame is
   ready to paint (see openLightbox in script.js) — full-res files take real
   time to decode, so without this the box appears smoothly but the photo
   itself would still just snap in the instant decoding finishes. */
.image-lightbox-box img {
  display: block; max-width: 100%; max-height: 90vh; max-height: 90dvh; object-fit: contain;
  opacity: 0;
  transform: scale(1.025);
  filter: blur(10px);
  transition: opacity .5s cubic-bezier(.2, .7, .2, 1), transform .5s cubic-bezier(.2, .7, .2, 1), filter .5s cubic-bezier(.2, .7, .2, 1);
}
.image-lightbox-box img.img-ready { opacity: 1; transform: scale(1); filter: blur(0); }
.image-lightbox-close { position: absolute; top: -14px; right: -14px; width: 34px; height: 34px; display: flex; align-items: center; justify-content: center; border: 0; border-radius: 999px; background: var(--paper); color: var(--ink); font-size: 1rem; cursor: pointer; box-shadow: 0 6px 18px rgba(0, 0, 0, .25); transition: transform .2s; }
.image-lightbox-close:hover { transform: scale(1.12); }
@media (prefers-reduced-motion: reduce) {
  .image-lightbox-backdrop, .image-lightbox-box { transition: opacity .15s linear; transform: none !important; }
  .image-lightbox-box img { transition: opacity .15s linear; transform: none !important; filter: none !important; opacity: 1; }
}

/* --- shopping cart: header icon + slide-in panel --- */
.cart-button {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color .2s ease, border-color .2s ease, transform .2s ease;
}
.cart-button:hover, .cart-button:focus-visible { color: var(--ink); border-color: var(--ink); }
/* the header cart is a pill like the nav buttons: icon + "Cart" (the
   side dock's cart stays round, see .dock-cart-button) */
.cart-button:not(.dock-cart-button) {
  --nav-color: #3964c3;
  width: auto;
  height: auto;
  gap: 7px;
  padding: 7px 14px;
  border-radius: 999px;
  border-color: color-mix(in srgb, var(--nav-color) 38%, var(--line) 62%);
  background: color-mix(in srgb, var(--nav-color) 11%, transparent);
  color: color-mix(in srgb, var(--nav-color) 68%, var(--ink) 32%);
  font: 600 .68rem Manrope, Arial, sans-serif;
  text-transform: uppercase;
  letter-spacing: .14em;
}
.cart-button:not(.dock-cart-button):hover, .cart-button:not(.dock-cart-button):focus-visible { background: var(--nav-color); border-color: transparent; color: #fff; }
.cart-button:not(.dock-cart-button) .cart-count { top: -7px; right: -7px; }
.cart-icon { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.cart-count {
  position: absolute;
  top: -6px;
  right: -6px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-acid);
  font: 700 .58rem 'DM Mono', monospace;
  display: flex;
  align-items: center;
  justify-content: center;
}
.cart-count[hidden] { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .cart-count.pop { animation: cartCountPop .4s cubic-bezier(.3, .7, .3, 1.4); }
}
@keyframes cartCountPop {
  0% { transform: scale(.5); }
  50% { transform: scale(1.35); }
  100% { transform: scale(1); }
}

.buy-button.in-cart { background: var(--ink); color: var(--paper); }

.cart-panel { position: fixed; inset: 0; z-index: 52; }
.cart-panel[hidden] { display: none; }
.cart-panel-backdrop { position: absolute; inset: 0; background: rgba(15, 15, 12, .55); opacity: 0; transition: opacity .3s ease; }
.cart-panel.visible .cart-panel-backdrop { opacity: 1; }
.cart-panel-box {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(380px, 100%);
  display: flex;
  flex-direction: column;
  padding: 34px 26px 26px;
  background: var(--paper);
  border-left: 1px solid var(--line);
  overflow-y: auto;
  transform: translateX(100%);
  transition: transform .35s cubic-bezier(.2, .7, .2, 1);
}
.cart-panel.visible .cart-panel-box { transform: translateX(0); }
.cart-panel-close { position: absolute; top: 14px; right: 14px; border: 0; background: none; font-size: .95rem; color: var(--muted); cursor: pointer; line-height: 1; padding: 8px; transition: color .2s, transform .2s; }
.cart-panel-close:hover { color: var(--ink); transform: scale(1.15); }
.cart-panel-box h3 { margin: 10px 0 24px; font-size: 1.5rem; font-weight: 500; letter-spacing: -.02em; }
.cart-items { display: flex; flex-direction: column; gap: 14px; flex: 1; }
.cart-item { display: flex; justify-content: space-between; align-items: center; gap: 10px; font-size: .82rem; padding-bottom: 14px; border-bottom: 1px solid var(--line); }
.cart-item-title { color: var(--ink); }
.cart-item-price { color: var(--muted); font: .7rem 'DM Mono', monospace; margin-left: auto; }
.cart-item-remove { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: .9rem; padding: 4px; line-height: 1; transition: color .2s; }
.cart-item-remove:hover { color: var(--ink); }
.cart-empty { color: var(--muted); font-size: .85rem; line-height: 1.6; }
.cart-total-row { display: flex; justify-content: space-between; align-items: baseline; margin: 22px 0 18px; font-size: .95rem; }
.cart-total-row strong { font-size: 1.3rem; font-weight: 500; }
.cart-checkout { border: 0; padding: 15px 18px; font: .68rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .1em; color: var(--on-acid); background: var(--acid); cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: opacity .2s, transform .2s; }
.cart-checkout:hover:not(:disabled) { transform: scale(1.02); }
.cart-checkout:disabled { opacity: .35; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) {
  .cart-panel-backdrop, .cart-panel-box { transition: opacity .15s linear; transform: none !important; }
}
@media (max-width: 700px) {
  .cart-panel-box { width: 100%; }
}

.policy-hero { padding: 10vh 4.5vw 8vh; border-bottom: 1px solid var(--line); }
.policy-hero h2 { margin-top: 3vh; }
/* Lines up under the intro paragraph on the right (same 52% left margin as
   .about-lede) instead of sitting alone at the left edge below the "Policy."
   heading, which read as an orphaned, scattered line with nothing to anchor
   it to. */
.policy-nav { display: flex; flex-wrap: wrap; gap: 16px 22px; width: min(460px, 48%); margin: 3vh 0 0 52%; font: .66rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .08em; }
.policy-nav a { color: var(--muted); border-bottom: 1px solid var(--line); padding-bottom: 4px; }
.policy-nav a:hover { color: var(--ink); }
.policy-section { padding: 9vh 4.5vw; border-top: 1px solid var(--line); }
.policy-section .policy-inner { max-width: 720px; }
.policy-section h2 { font-size: clamp(1.9rem, 3.6vw, 2.8rem); margin-top: 14px; }
.policy-section p { font-size: .92rem; line-height: 1.85; color: var(--muted); margin: 18px 0; }
.policy-section p strong { color: var(--ink); font-weight: 600; }
.policy-section ul { margin: 18px 0; padding-left: 22px; color: var(--muted); font-size: .92rem; line-height: 1.85; }
.policy-section li { margin-bottom: 8px; }
.policy-section .policy-callout { margin: 26px 0; padding: 18px 22px; border: 1px solid var(--line); font-size: .88rem; line-height: 1.75; color: var(--ink); }
.policy-section .policy-scope-note { font-style: italic; font-size: .82rem; padding-left: 14px; border-left: 2px solid var(--line); }
.faq-list { display: flex; flex-direction: column; gap: 10px; margin-top: 26px; }
.faq-item { border: 1px solid var(--line); border-radius: 8px; padding: 4px 20px; }
.faq-item summary {
  padding: 16px 26px 16px 0;
  font-size: .95rem;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
  position: relative;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: '+';
  position: absolute;
  right: 0;
  top: 16px;
  font-size: 1.1rem;
  color: var(--muted);
  transition: transform .2s ease;
}
.faq-item[open] summary::after { content: '\2212'; }
.faq-item summary:hover::after { color: var(--ink); }
.faq-item p { margin: 0 0 18px; font-size: .88rem; line-height: 1.8; }
.faq-item p code { font: 500 .82em 'DM Mono', monospace; background: color-mix(in srgb, var(--acid) 10%, transparent); padding: 2px 5px; border-radius: 3px; }
@media (prefers-reduced-motion: reduce) {
  .faq-item summary::after { transition: none; }
}

@media (max-width: 700px) {
  .policy-hero { padding: 9vh 6vw 6vh; }
  .policy-section { padding: 7vh 6vw; }
}

/* the gap this closes: neither "full desktop" (nav fits on one line) nor
   the phone breakpoint below (which also collapses the gallery grid, hero,
   etc.) — a half-screened desktop window, a tablet, a foldable's outer
   screen, anything in between. Only the header/nav wraps here; the rest of
   the page layout is untouched at these widths. */
@media (max-width: 1360px) and (min-width: 701px) {
  .site-header { grid-template-columns: 1fr; row-gap: 14px; padding: 20px 4.5vw; }
  .brand { justify-self: center; }
  .site-nav { justify-content: center; flex-wrap: wrap; row-gap: 8px; }
  .header-contact-group { justify-self: center; }
}

@media (max-width: 700px) {
  .site-header { grid-template-columns: 1fr auto; gap: 18px; padding: 22px 6vw; }
  .site-nav { grid-column: 1 / -1; grid-row: 2; justify-content: center; flex-wrap: wrap; gap: 6px; }
  .site-nav a { padding: 6px 7px; }
  .nav-dot { margin-left: 3px; }
  .brand { gap: 8px; }
  .brand-links { gap: 0; }
  .brand-link { width: 23px; height: 23px; }
  .brand-link svg { width: 13px; height: 13px; }
  .header-contact { font-size: .58rem; }
  .header-contact-group { gap: 10px; }
  .hero { min-height: 73vh; min-height: 73dvh; padding: 8vh 6vw 5vh; }
  .about-hero h1, .blank-content h1 { font-size: clamp(3.8rem, 17vw, 6rem); }
  .about-lede { margin-left: 25%; width: 75%; }
  .hero-intro-title { font-size: clamp(2.6rem, 13vw, 4.2rem); }
  .policy-nav { margin-left: 25%; width: 70%; }
  .about-hero { padding: 11vh 6vw; }
  /* Right padding is wider than left on purpose: the floating .nav-dock
     sits at `right: 3vw` with 36px icons (see its mobile rule below), so
     card content needs to clear roughly 3vw + 36px + a gutter or the buy
     button/badges visually collide with the dock icons — spotted on an
     actual phone, where the "Add to cart" button was overlapping the dock's
     cart icon. */
  .collection-section, .market-page { padding: 11vh calc(3vw + 48px) 11vh 6vw; }
  .section-heading { display: block; }
  .section-intro { margin: 30px 0 0; width: 80%; }
  .gallery-grid { display: block; }
  .gallery-card { margin: 60px 0 0; }
  .gallery-card:first-child { margin-top: 0; }
  .gallery-card .image-wrap { aspect-ratio: 1.05; }
  .card-portrait .image-wrap, .card-tall .image-wrap { aspect-ratio: .82; }
  .buy-button { opacity: 1; transform: none; }
  .reactions { opacity: 1; transform: none; }
  .card-caption { font-size: .6rem; }
  .manifesto { padding: 14vh 8vw 16vh; }
  .about-story, .specs-section, .elisa-section { display: block; padding: 11vh 8vw; }
  .story-image { margin-bottom: 11vh; }
  .spec-list { margin-top: 55px; }
  .spec-row { font-size: .66rem; }
  .about-cta { padding: 13vh 6vw; }
  .site-footer { flex-wrap: wrap; gap: 14px; padding: 22px 6vw; }
  .site-footer span:nth-child(2) { display: none; }
}




/* --- photo tips above section dividers (see script.js) ---
   Small, quiet advice sitting just above a section's dividing line. */
.has-photo-tip { position: relative; }
.photo-tip {
  position: absolute;
  left: 4.5vw;
  right: 4.5vw;
  margin: 0;
  color: var(--muted);
  max-width: none;
  font: 400 .68rem/1.5 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .08em;
  text-align: left;
  pointer-events: none;
}
.photo-tip::before { content: '— '; color: var(--acid); }
/* line on the section's top edge: sit just above it */
.photo-tip--above { top: 0; transform: translateY(calc(-100% - 12px)); }
/* line on the section's bottom edge: sit just inside, above it */
.photo-tip--inside { bottom: 12px; }
@media (max-width: 700px) {
  .photo-tip { font-size: .6rem; right: calc(3vw + 48px); }
}

/* --- hero title wave ---
   Each page's big hero title sits still, then springs to life letter by
   letter (pop, settle, two small bobs, like the refresh screen's A/AO
   letters, with a brief accent-colour flash), then stops. Repeats every 8s.
   script.js splits the titles into letter spans. Market waits for its loading screen (market-revealed). */
.wave-word { display: inline-block; white-space: nowrap; }
.wave-letter { display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  .hero-intro-title .wave-letter,
  .about-hero h1 .wave-letter,
  .blank-content h1 .wave-letter,
  .policy-hero h2 .wave-letter,
  .policy-section h2 .wave-letter,
  body.market-revealed .market-page > .section-heading h2 .wave-letter {
    animation: titleWave 8s ease-in-out infinite;
    animation-delay: calc(2s + var(--i) * .055s);
  }
  /* section headings further down the page: they alternate between the
     4s and 8s slots (set as --phase by script.js), so with the hero (2s)
     and the footer glare (6s) something moves every 2s, never two at once */
  #collection-title .wave-letter,
  .specs-section h2 .wave-letter,
  .elisa-section h2 .wave-letter,
  .about-cta h2 .wave-letter {
    animation: titleWave 8s ease-in-out infinite;
    animation-delay: calc(var(--phase, 4s) + var(--i) * .055s);
  }
  /* FAQ questions: a quicker ripple per question, and each question
     starts a beat after the one above, so the wave runs down the list */
  .faq-item summary .wave-letter {
    animation: titleWave 8s ease-in-out infinite;
    animation-delay: calc(2.5s + var(--q) * .35s + var(--i) * .02s);
  }
}
/* each page's waves glow in the colour of its own header button (the
   --nav-color values on .site-nav a[data-nav=...]); set on body so the
   titles, eyebrows and arrows all inherit it */
body:has(.site-nav a.active[data-nav="collection"]) { --wave-color: #3964c3; }
body:has(.site-nav a.active[data-nav="about"]) { --wave-color: #c9793c; }
body:has(.site-nav a.active[data-nav="market"]) { --wave-color: #8a5cd0; }
body:has(.site-nav a.active[data-nav="archive"]) { --wave-color: #8a6f52; }
body:has(.site-nav a.active[data-nav="elisa"]) { --wave-color: #c9457a; }
body:has(.site-nav a.active[data-nav="policy"]) { --wave-color: #3f9c8c; }
/* Elisa's sections on the About and Policy pages keep her pink */
.elisa-section, #elisa-terms { --wave-color: #c9457a; }
@keyframes titleWave {
  0%     { transform: none; color: inherit; }
  2.5%   { transform: scale(1.14) translateY(-.14em); color: var(--wave-color, var(--acid)); }
  4.5%   { transform: scale(.96) translateY(.03em); }
  6.5%   { transform: scale(1) translateY(0); color: inherit; }
  9%     { transform: translateY(-.07em); }
  11.5%  { transform: translateY(0); }
  14%    { transform: translateY(-.04em); }
  16.5%, 100% { transform: none; }
}

/* footer copyright: every 8s a red glare travels across it left to right,
   each letter hopping as it passes (the same red as the stuck-at-91%
   number on the Coming Soon page). Letters are split by script.js. */
@media (prefers-reduced-motion: no-preference) {
  .site-footer > span:first-child .wave-letter {
    animation: copyrightWave 8s ease-in-out infinite;
    animation-delay: calc(6s + var(--i) * .035s); /* 6s: halfway between the title waves (2s, 10s...), so they never fire together */
  }
}
@keyframes copyrightWave {
  0%, 12%, 100% { transform: none; color: inherit; text-shadow: none; }
  3%  { transform: translateY(-4px); color: #d0463b; text-shadow: 0 0 10px rgba(208, 70, 59, .6); }
  6%  { transform: translateY(1px); color: #d0463b; text-shadow: 0 0 6px rgba(208, 70, 59, .4); }
  8%  { transform: none; }
}

/* eyebrow labels and link arrows ride along with their nearest heading's
   wave (timings set by script.js): the eyebrow hops just before the
   heading waves, the arrow nudges just after it settles. No new beats. */
.wave-eyebrow, .wave-arrow { display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  .wave-eyebrow { animation: eyebrowHop 8s ease-in-out infinite; animation-delay: var(--eb-delay, 2s); }
  .wave-arrow { animation: arrowNudge 8s ease-in-out infinite; animation-delay: var(--arrow-delay, 3s); }
}
@keyframes eyebrowHop {
  0%, 8%, 100% { transform: none; color: inherit; }
  2.5% { transform: translateY(-3px); color: var(--wave-color, var(--acid)); }
  5%   { transform: translateY(0); color: var(--wave-color, var(--acid)); }
}
@keyframes arrowNudge {
  0%, 9%, 100% { transform: none; }
  3% { transform: translate(4px, -4px); }
  6% { transform: translate(-1px, 1px); }
}

/* --- the menu's idle moves ---
   Every few seconds script.js picks one random on-screen menu button and
   one random move (never the same move twice in a row). Uses the separate
   rotate/scale/translate properties so it never fights the buttons' own
   hover transforms. Pulse uses the button's own --nav-color. */
@media (prefers-reduced-motion: no-preference) {
  .idle-shake   { animation: idleShake .7s ease-in-out; }
  .idle-spin    { animation: idleSpin .9s cubic-bezier(.4, 0, .2, 1); }
  .idle-pop     { animation: idlePop .6s cubic-bezier(.3, .7, .3, 1.4); }
  .idle-tilt    { animation: idleTilt 1.2s ease-in-out; }
  .idle-pulse   { animation: idlePulse 1.1s ease-out; }
  .idle-squeeze { animation: idleSqueeze .8s ease-in-out; }
  .idle-hop     { animation: idleHop .75s cubic-bezier(.3, .7, .3, 1); }
  .idle-dip     { animation: idleDip .7s ease-in-out; }
  .idle-float   { animation: idleFloat 1.6s ease-in-out; }
}
@keyframes idleShake {
  0%, 100% { rotate: 0deg; scale: 1; }
  12% { rotate: calc(-14deg * var(--dir, 1)); scale: 1.12; }
  30% { rotate: calc(12deg * var(--dir, 1)); scale: 1.12; }
  48% { rotate: calc(-8deg * var(--dir, 1)); scale: 1.06; }
  66% { rotate: calc(5deg * var(--dir, 1)); scale: 1.02; }
  84% { rotate: calc(-2deg * var(--dir, 1)); scale: 1; }
}
@keyframes idleSpin {
  0% { rotate: 0deg; scale: 1; }
  45% { scale: 1.1; }
  80% { rotate: calc(380deg * var(--dir, 1)); scale: 1; }
  100% { rotate: calc(360deg * var(--dir, 1)); }
}
@keyframes idlePop {
  0%, 100% { scale: 1; }
  35% { scale: 1.25; }
  60% { scale: .92; }
  80% { scale: 1.04; }
}
@keyframes idleTilt {
  0%, 100% { rotate: 0deg; }
  20%, 60% { rotate: calc(-12deg * var(--dir, 1)); }
  75% { rotate: calc(4deg * var(--dir, 1)); }
  88% { rotate: calc(-1deg * var(--dir, 1)); }
}
@keyframes idlePulse {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--nav-color, var(--acid)) 60%, transparent); scale: 1; }
  25% { scale: 1.06; }
  100% { box-shadow: 0 0 0 14px transparent; scale: 1; }
}
@keyframes idleSqueeze {
  0%, 100% { scale: 1 1; }
  20% { scale: 1.25 .75; }
  45% { scale: .85 1.2; }
  65% { scale: 1.08 .94; }
  82% { scale: .97 1.03; }
}
@keyframes idleHop {
  0%, 100% { translate: 0 0; scale: 1 1; }
  15% { translate: 0 0; scale: 1.1 .88; }
  45% { translate: 0 -12px; scale: .94 1.08; }
  75% { translate: 0 0; scale: 1.12 .9; }
  88% { scale: .98 1.02; }
}
@keyframes idleDip {
  0%, 100% { translate: 0 0; }
  35% { translate: 0 6px; }
  65% { translate: 0 -2px; }
  85% { translate: 0 1px; }
}
@keyframes idleFloat {
  0%, 100% { translate: 0 0; scale: 1; }
  35%, 60% { translate: 0 -7px; scale: 1.04; }
  85% { translate: 0 1px; scale: 1; }
}

/* "escape": a menu button breaks loose from its spot (top pills drop down,
   side dock buttons slide left; script.js sets --ex/--ey), wobbles there
   like it's broken, then crouches and jumps back into place */
@media (prefers-reduced-motion: no-preference) {
  .idle-escape { animation: idleEscape 2.4s cubic-bezier(.4, 0, .3, 1); }
}
@keyframes idleEscape {
  0%   { translate: 0 0; rotate: 0deg; scale: 1 1; }
  6%   { translate: 0 0; rotate: calc(-4deg * var(--dir, 1)); }
  10%  { translate: 0 0; rotate: calc(4deg * var(--dir, 1)); }
  28%  { translate: calc(var(--ex) * 1.08) calc(var(--ey) * 1.08); rotate: calc(16deg * var(--dir, 1)); }
  34%  { translate: var(--ex) var(--ey); rotate: calc(11deg * var(--dir, 1)); }
  42%  { rotate: calc(15deg * var(--dir, 1)); }
  48%  { rotate: calc(9deg * var(--dir, 1)); }
  54%  { rotate: calc(13deg * var(--dir, 1)); }
  64%  { translate: var(--ex) var(--ey); rotate: calc(12deg * var(--dir, 1)); scale: 1 1; }
  72%  { translate: var(--ex) var(--ey); rotate: 0deg; scale: 1.14 .82; }
  84%  { translate: calc(var(--ex) * -.12) calc(var(--ey) * -.12 - 10px); rotate: calc(-6deg * var(--dir, 1)); scale: .9 1.12; }
  93%  { translate: 0 0; rotate: 0deg; scale: 1.1 .9; }
  100% { translate: 0 0; rotate: 0deg; scale: 1 1; }
}

/* social icons join the menu's idle moves; their pulse ring uses each
   brand's own colour */
.brand-link { --nav-color: var(--brand-color); }
/* "stroll": a social icon hops down out of its row, lights up in its brand
   colour, takes three hop-steps across (--sx, set by script.js), looks
   around, then hops back home */
@media (prefers-reduced-motion: no-preference) {
  .brand-link.idle-stroll { z-index: 6; color: #fff; animation: idleStroll 3.4s ease-in-out; }
  .brand-link.idle-stroll::before { opacity: 1; transform: scale(1); }
}
@keyframes idleStroll {
  0%   { translate: 0 0; rotate: 0deg; scale: 1 1; }
  6%   { translate: 0 0; scale: 1.12 .86; }
  14%  { translate: calc(var(--sx) * .05) 30px; scale: .94 1.08; }
  18%  { translate: calc(var(--sx) * .08) 34px; scale: 1.1 .9; }
  /* three hop-steps across */
  26%  { translate: calc(var(--sx) * .3) 24px; scale: 1 1; rotate: calc(8deg * var(--dir, 1)); }
  32%  { translate: calc(var(--sx) * .4) 34px; scale: 1.08 .92; rotate: 0deg; }
  40%  { translate: calc(var(--sx) * .62) 24px; scale: 1 1; rotate: calc(-8deg * var(--dir, 1)); }
  46%  { translate: calc(var(--sx) * .72) 34px; scale: 1.08 .92; rotate: 0deg; }
  54%  { translate: calc(var(--sx) * .94) 24px; scale: 1 1; rotate: calc(8deg * var(--dir, 1)); }
  60%  { translate: var(--sx) 34px; scale: 1.08 .92; rotate: 0deg; }
  /* looks around */
  66%  { translate: var(--sx) 34px; scale: 1 1; rotate: -12deg; }
  72%  { translate: var(--sx) 34px; rotate: 12deg; }
  78%  { translate: var(--sx) 34px; rotate: 0deg; scale: 1.14 .84; }
  /* big hop home */
  90%  { translate: calc(var(--sx) * .35) -14px; scale: .9 1.12; rotate: calc(-10deg * var(--dir, 1)); }
  96%  { translate: 0 0; scale: 1.1 .9; rotate: 0deg; }
  100% { translate: 0 0; scale: 1 1; rotate: 0deg; }
}

/* "stadium wave": the social icons stand up one after another, left to
   right, lighting up in their brand colour at the top, then sit back down
   (script.js staggers them, sometimes twice round) */
@media (prefers-reduced-motion: no-preference) {
  .brand-link.idle-cheer { animation: idleCheer .75s cubic-bezier(.3, .7, .3, 1); }
  .brand-link.idle-cheer::before { animation: cheerLight .75s ease-out; }
  .brand-link.idle-cheer svg { animation: cheerInk .75s ease-out; }
}
@keyframes idleCheer {
  0%, 100% { translate: 0 0; scale: 1 1; }
  15% { translate: 0 2px; scale: 1.08 .9; }
  45% { translate: 0 -14px; scale: .92 1.15; }
  75% { translate: 0 0; scale: 1.06 .94; }
}
@keyframes cheerLight {
  0%, 100% { opacity: 0; transform: scale(.6); }
  35%, 60% { opacity: 1; transform: scale(1); }
}
@keyframes cheerInk {
  0%, 100% { color: inherit; }
  35%, 60% { color: #fff; }
}

/* social icon chats: two neighbouring icons trade little speech bubbles
   (typing dots, laughs, smiles...), the speaker bobbing as it "talks".
   Bubbles are created and removed by script.js. */
.chat-bubble {
  position: absolute;
  /* below the icon: the social icons sit near the top edge of the page,
     so there's no room for bubbles above them */
  top: calc(100% + 9px);
  left: 50%;
  z-index: 7;
  display: flex;
  align-items: center;
  gap: 3px;
  min-height: 22px;
  padding: 3px 8px;
  border-radius: 11px;
  background: var(--ink);
  color: var(--paper);
  font: 600 .7rem/1 Manrope, Arial, sans-serif;
  white-space: nowrap;
  pointer-events: none;
  translate: -50% 0;
  transform-origin: 50% 0;
  animation: bubbleIn .9s cubic-bezier(.3, .7, .3, 1.3) forwards;
}
.chat-bubble::after {
  content: '';
  position: absolute;
  bottom: 100%;
  left: 50%;
  translate: -50% 1px;
  border: 5px solid transparent;
  border-bottom-color: var(--ink);
}
.chat-bubble .emoji { font-size: .95rem; line-height: 1; }
.chat-dots { display: flex; gap: 3px; padding: 0 1px; }
.chat-dots i { width: 4px; height: 4px; border-radius: 50%; background: currentColor; animation: chatDot .9s ease-in-out infinite; }
.chat-dots i:nth-child(2) { animation-delay: .15s; }
.chat-dots i:nth-child(3) { animation-delay: .3s; }
@keyframes chatDot {
  0%, 60%, 100% { translate: 0 0; opacity: .5; }
  30% { translate: 0 -3px; opacity: 1; }
}
@keyframes bubbleIn {
  0% { opacity: 0; scale: .3; }
  18% { opacity: 1; scale: 1.08; }
  28%, 82% { opacity: 1; scale: 1; }
  100% { opacity: 0; scale: .7; }
}
@media (prefers-reduced-motion: no-preference) {
  .brand-link.chat-talking { animation: chatTalk .9s ease-in-out; }
}
@keyframes chatTalk {
  0%, 100% { translate: 0 0; rotate: 0deg; }
  20% { translate: 0 -3px; rotate: calc(-6deg * var(--dir, 1)); }
  40% { translate: 0 0; rotate: 0deg; }
  60% { translate: 0 -2px; rotate: calc(5deg * var(--dir, 1)); }
}
@media (prefers-reduced-motion: reduce) {
  .chat-bubble { display: none; }
}

/* policy: "Latest updates summary :)" dropdown under the updated date.
   The table inside is generated by tools/stamp-policy-date.js from
   tools/policy-changelog.json. */
.policy-changes { width: min(410px, 48%); margin: 2.2vh 0 0 52%; }
.policy-changes summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: #d0463b;
  font: 500 .72rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .1em;
  cursor: pointer;
  list-style: none;
}
.policy-changes summary::-webkit-details-marker { display: none; }
.policy-changes summary::after { content: '\25BE'; transition: rotate .25s ease; }
.policy-changes[open] summary::after { rotate: 180deg; }
.policy-changes summary:hover { text-decoration: underline; text-underline-offset: 3px; }
.policy-changes table {
  width: 100%;
  margin-top: 12px;
  border-collapse: collapse;
  font-size: .8rem;
  line-height: 1.45;
}
.policy-changes th {
  padding: 0 10px 6px 0;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font: 500 .62rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .1em;
  text-align: left;
}
.policy-changes td { padding: 8px 10px 8px 0; border-bottom: 1px solid var(--line); vertical-align: top; }
.policy-changes td:first-child { width: 45%; color: var(--ink); font-weight: 600; }
.policy-changes td:last-child { color: var(--muted); }
/* the list drops open and rolls back up slowly (script.js toggles
   .expanded; animating grid rows from 0fr to 1fr gives a smooth height) */
.changes-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .75s cubic-bezier(.3, .7, .3, 1); }
.changes-inner { min-height: 0; overflow: hidden; }
.policy-changes.expanded .changes-body { grid-template-rows: 1fr; }
@media (prefers-reduced-motion: no-preference) {
  .policy-changes .changes-inner > * { opacity: 0; translate: 0 -6px; transition: opacity .35s ease, translate .35s ease; }
  .policy-changes.expanded .changes-inner > * { opacity: 1; translate: 0 0; }
  .policy-changes.expanded .changes-inner tbody tr { animation: changesRow .5s cubic-bezier(.3, .7, .3, 1) both; }
  .policy-changes.expanded .changes-inner tbody tr:nth-child(n) { animation-delay: calc(.25s + var(--row, 0) * .06s); }
}
@media (prefers-reduced-motion: reduce) {
  .changes-body { transition: none; }
}
@keyframes changesRow { from { opacity: 0; translate: 0 -8px; } to { opacity: 1; translate: 0 0; } }
@keyframes changesDrop {
  from { opacity: 0; translate: 0 -6px; }
  to { opacity: 1; translate: 0 0; }
}
@media (max-width: 700px) {
  .policy-changes { margin-left: 25%; width: 75%; }
}

.policy-changes .changes-meta {
  margin: 12px 0 0;
  color: var(--ink);
  font: 600 .66rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .12em;
}
/* bottom-left "VERSION N" on the policy page: red, with a glowing
   left-to-right shimmer on the footer's beat (6s), so the two read as one
   movement. Letters are split by script.js. */
.policy-version-badge {
  margin: 0;
  padding: 0 4.5vw 7vh;
  color: #d0463b;
  font: 500 .78rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .16em;
  text-shadow: 0 0 6px rgba(208, 70, 59, .35);
}
@media (prefers-reduced-motion: no-preference) {
  .policy-version-badge .wave-letter {
    animation: versionGlow 8s ease-in-out infinite;
    animation-delay: calc(5.6s + var(--i) * .06s);
  }
}
@keyframes versionGlow {
  0%, 12%, 100% { transform: none; text-shadow: 0 0 6px rgba(208, 70, 59, .35); }
  3% { transform: translateY(-3px); text-shadow: 0 0 14px rgba(208, 70, 59, .95), 0 0 4px rgba(255, 140, 120, .9); }
  7% { transform: none; text-shadow: 0 0 9px rgba(208, 70, 59, .6); }
}
@media (max-width: 700px) {
  .policy-version-badge { padding: 0 6vw 6vh; }
}
/* red "VERSION: N" next to the policy title (kept out of the title wave
   with .no-wave; the number is kept in sync by tools/stamp-policy-date.js) */
.policy-title-version {
  display: inline-block;
  margin: .55em 0 0 .7em;
  vertical-align: top;
  color: #d0463b;
  font: 500 clamp(.68rem, 1vw, .85rem) 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .16em;
  text-shadow: 0 0 8px rgba(208, 70, 59, .45);
}

/* the summary's smiley: a drawn 😊 that can actually move. Opening the
   summary (script.js adds .thinking) makes it open its eyes, look down at
   the table and scan it, a "?" pops up above its head, then it settles
   back into the happy smile. */
.changes-emoji { position: relative; display: inline-flex; width: 1.5rem; height: 1.5rem; }
.smiley { width: 100%; height: 100%; overflow: visible; }
.sm-face { fill: #ffcc4d; }
.sm-cheek { fill: #ff7892; opacity: .45; }
.sm-eyes-closed path, .sm-smile, .sm-hmm { fill: none; stroke: #664500; stroke-width: 2.2; stroke-linecap: round; }
.sm-eye { fill: #664500; }
.sm-eyes-open, .sm-hmm { opacity: 0; }
.sm-eyes-open, .sm-eyes-closed, .sm-smile, .sm-hmm { transform-box: view-box; transform-origin: 18px 14px; }
.sm-think {
  position: absolute;
  bottom: 92%;
  left: 62%;
  color: #d0463b;
  font: 800 .95rem/1 Manrope, Arial, sans-serif;
  opacity: 0;
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .policy-changes.thinking .sm-eyes-closed { animation: smClosed 2.8s ease-in-out forwards; }
  .policy-changes.thinking .sm-eyes-open { animation: smOpen 2.8s ease-in-out forwards; }
  .policy-changes.thinking .sm-eye { animation: smLook 2.8s ease-in-out forwards; }
  .policy-changes.thinking .sm-smile { animation: smSmile 2.8s ease-in-out forwards; }
  .policy-changes.thinking .sm-hmm { animation: smHmm 2.8s ease-in-out forwards; }
  .policy-changes.thinking .sm-think { animation: smThink 2.8s ease-in-out forwards; }
  .policy-changes.thinking .smiley { animation: smBounce 2.8s ease-in-out forwards; }
}
/* happy closed eyes hide while the open eyes show */
@keyframes smClosed { 0%, 88% { opacity: 1; } 6%, 82% { opacity: 0; } }
@keyframes smOpen {
  0%, 4% { opacity: 0; transform: scaleY(.1); }
  10% { opacity: 1; transform: scaleY(1.15); }
  14%, 80% { opacity: 1; transform: scaleY(1); }
  /* a quick blink mid-read */
  50% { transform: scaleY(1); } 53% { transform: scaleY(.15); } 56% { transform: scaleY(1); }
  86%, 100% { opacity: 0; transform: scaleY(.1); }
}
/* looks down at the table, scans left and right, looks back up */
@keyframes smLook {
  0%, 14% { transform: translate(0, 0); }
  22% { transform: translate(0, 2.2px); }
  34% { transform: translate(-1.6px, 2.4px); }
  46% { transform: translate(1.6px, 2.4px); }
  60% { transform: translate(-1.2px, 2.4px); }
  70% { transform: translate(0, 2px); }
  80%, 100% { transform: translate(0, 0); }
}
@keyframes smSmile { 0%, 86%, 100% { opacity: 1; } 12%, 80% { opacity: 0; } }
@keyframes smHmm { 0%, 10%, 86%, 100% { opacity: 0; } 16%, 80% { opacity: 1; } }
@keyframes smThink {
  0%, 18% { opacity: 0; translate: 0 4px; scale: .3; }
  26% { opacity: 1; translate: 0 -2px; scale: 1.2; }
  32% { scale: 1; translate: 0 0; }
  44% { translate: 0 -3px; rotate: -10deg; }
  56% { translate: 0 0; rotate: 8deg; }
  68% { translate: 0 -3px; rotate: -6deg; }
  76% { opacity: 1; translate: 0 0; rotate: 0deg; }
  84%, 100% { opacity: 0; translate: 0 -8px; scale: .6; }
}
@keyframes smBounce {
  0%, 100% { transform: none; }
  8% { transform: translateY(-2px) scale(1.12); }
  14% { transform: none; }
  88% { transform: rotate(-8deg); }
  94% { transform: rotate(4deg) scale(1.06); }
}

/* policy paragraph labels ("The short version", "Who we are"...): their
   own style, small caps in the page colour on a line of their own, so
   they're never confused with the section headings or bold words */
.policy-lead {
  display: block;
  margin-bottom: 6px;
  color: var(--wave-color, var(--acid));
  font: 600 .7rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .14em;
}

/* policy cross-references: "see How long we keep it ↓" links between
   paragraphs. The page glides to the target, which glows briefly in the
   page colour so your eye lands on it (a reaction, not a loop). */
.xref {
  color: var(--wave-color, var(--acid));
  font: 500 .72em 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .08em;
  white-space: nowrap;
  border-bottom: 1px dashed currentColor;
}
.xref:hover { border-bottom-style: solid; }
.policy-section [id] { scroll-margin-top: 90px; }
@media (prefers-reduced-motion: no-preference) {
  html:has(.policy-section) { scroll-behavior: smooth; }
  .policy-section [id]:target { animation: xrefArrive 2.6s ease-out; border-radius: 8px; }
}
/* arrival glow: a clear teal wash plus an outline ring, held for a beat
   before fading, so it's easy to spot which text a link pointed to */
@keyframes xrefArrive {
  0% { background: transparent; box-shadow: 0 0 0 0 transparent, 0 0 0 0 transparent; }
  12%, 55% {
    background: color-mix(in srgb, var(--wave-color, var(--acid)) 13%, transparent);
    box-shadow: 0 0 0 8px color-mix(in srgb, var(--wave-color, var(--acid)) 13%, transparent), 0 0 0 9.5px color-mix(in srgb, var(--wave-color, var(--acid)) 70%, transparent);
  }
  100% { background: transparent; box-shadow: 0 0 0 8px transparent, 0 0 0 9.5px transparent; }
}

/* policy: the signed data commitment at the top of Privacy */
.policy-commitment {
  margin: 26px 0;
  padding: 20px 22px;
  border: 1px solid color-mix(in srgb, var(--wave-color, var(--acid)) 35%, var(--line));
  border-left: 4px solid var(--wave-color, var(--acid));
  border-radius: 10px;
  background: color-mix(in srgb, var(--wave-color, var(--acid)) 6%, transparent);
}
.policy-commitment p:first-child { margin-top: 0; }
.policy-commitment ul { margin-bottom: 0; }
.policy-commitment .xref { margin-left: 8px; }
.commitment-sign {
  margin: 16px 0 0;
  text-align: right;
  font: italic 400 1.05rem var(--serif, Georgia), serif;
}
.commitment-sign::before { content: '\2014\00a0'; }
/* key safety facts in the policy, in the site's red */
.policy-red { color: #d0463b; }

/* --- easter egg 3 / 5: the smiley at the end of the homepage intro.
   Click it: it jumps, a party hat drops onto its head, confetti bursts in
   the site's colours and the badge pops up, then it all settles back.
   Reuses the drawn smiley parts (.sm-*) from the policy page. --- */
.egg-smiley {
  position: relative;
  display: inline-flex;
  width: 1.5em;
  height: 1.5em;
  margin-left: .35em;
  padding: 0;
  vertical-align: -.32em;
  border: 0;
  background: none;
  cursor: pointer;
}
.egg-smiley .smiley { width: 100%; height: 100%; transition: transform .25s cubic-bezier(.3, .7, .3, 1.4); }
.egg-smiley:hover .smiley, .egg-smiley:focus-visible .smiley { transform: rotate(-10deg) scale(1.1); }
.sm-hat { opacity: 0; transform-box: view-box; transform-origin: 18px 4px; }
.sm-hat-body { fill: #8a5cd0; }
.sm-hat-stripe { fill: none; stroke: #ffcc4d; stroke-width: 2; stroke-linecap: round; }
.sm-hat-pom { fill: #c9457a; }
.egg-confetti { position: absolute; inset: 50% auto auto 50%; pointer-events: none; }
.egg-confetti i { position: absolute; width: 5px; height: 8px; border-radius: 1px; background: var(--c); opacity: 0; }
.egg3-badge {
  position: absolute;
  top: calc(100% + 12px); /* below the smiley, so it never covers the intro text */
  left: 50%;
  translate: -50% -6px;
  z-index: 20;
  white-space: nowrap;
  padding: 7px 13px;
  border-radius: 999px;
  font: 600 .64rem 'DM Mono', monospace;
  color: var(--on-accent-surface);
  background: var(--accent-surface);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s ease, translate .3s cubic-bezier(.3, .7, .3, 1.4);
}
.egg-smiley.party .egg3-badge { opacity: 1; translate: -50% 0; }
@media (prefers-reduced-motion: no-preference) {
  .egg-smiley.party .smiley { animation: partyJump 3.2s ease-in-out; }
  .egg-smiley.party .sm-hat { animation: partyHat 3.2s cubic-bezier(.3, .7, .3, 1.2); }
  .egg-smiley.party .egg-confetti i { animation: partyConfetti 1.4s cubic-bezier(.2, .7, .3, 1) .35s; }
}
@keyframes partyJump {
  0%, 100% { transform: none; }
  7%  { transform: translateY(2px) scale(1.1, .88); }
  14% { transform: translateY(-9px) scale(.94, 1.1); }
  22% { transform: translateY(0) scale(1.08, .92); }
  28% { transform: none; }
  38% { transform: rotate(-9deg); }
  48% { transform: rotate(9deg); }
  58% { transform: rotate(-7deg); }
  68% { transform: rotate(5deg); }
  76% { transform: none; }
}
@keyframes partyHat {
  0%, 10% { opacity: 0; transform: translateY(-16px) rotate(-20deg); }
  18% { opacity: 1; transform: translateY(1px) rotate(6deg); }
  24% { transform: translateY(0) rotate(-4deg); }
  30%, 76% { opacity: 1; transform: translateY(0) rotate(0deg); }
  86% { opacity: 1; transform: translateY(-10px) rotate(14deg); }
  100% { opacity: 0; transform: translateY(-18px) rotate(24deg); }
}
@keyframes partyConfetti {
  0% { opacity: 1; transform: translate(0, 0) rotate(0deg); }
  70% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--x), calc(var(--y) + 30px)) rotate(var(--r)); }
}

/* --- easter egg 1 / 5: the A/AO show (see script.js playEggOne) --- */
.egg1-stage {
  position: fixed;
  inset: 0;
  z-index: 300;
  pointer-events: none;
  background: color-mix(in srgb, var(--paper) 0%, transparent);
  transition: background .4s ease, backdrop-filter .4s ease;
}
.egg1-stage.on { background: color-mix(in srgb, var(--paper) 55%, transparent); backdrop-filter: blur(3px); }
.egg1-mark {
  position: fixed;
  display: flex;
  align-items: center;
  justify-content: center;
  transform-origin: center;
  white-space: nowrap;
}
.egg1-letter { display: inline-block; }
.egg1-slash { color: var(--acid); margin: 0 2px; }
.egg1-mark.dancing .egg1-letter { animation: egg1Dance 1.9s ease-in-out calc(var(--i) * .12s) both; }
@keyframes egg1Dance {
  0%, 100% { transform: none; }
  10% { transform: translateY(-.35em) rotate(-14deg); color: var(--c); }
  20% { transform: translateY(0) scale(1.12, .88); }
  30% { transform: translateY(-.25em) rotate(12deg); color: var(--c); }
  40% { transform: translateY(0) scale(1.08, .92); }
  55% { transform: scale(1.25) rotate(-8deg); color: var(--c); }
  66% { transform: scale(.95) rotate(4deg); }
  78% { transform: translateY(-.12em); }
  88% { transform: none; }
}
.egg1-burst { position: fixed; left: 50%; top: 50%; }
.egg1-burst i {
  position: absolute;
  width: 8px;
  height: 12px;
  border-radius: 2px;
  background: var(--c);
  opacity: 0;
}
.egg1-stage.bursting .egg1-burst i { animation: egg1Confetti 1.7s cubic-bezier(.15, .7, .3, 1) var(--d) both; }
@keyframes egg1Confetti {
  0% { opacity: 1; transform: translate(0, 0) rotate(0deg) scale(.4); }
  60% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--x), calc(var(--y) + 90px)) rotate(var(--r)) scale(1); }
}

/* the logo's "texting" hint bubble (script.js) */
.logo-hint {
  position: absolute;
  z-index: 60;
  max-width: 250px;
  min-height: 30px;
  display: flex;
  align-items: center;
  padding: 7px 13px;
  border-radius: 14px;
  background: var(--ink);
  color: var(--paper);
  font: 600 .74rem Manrope, Arial, sans-serif;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  scale: .7;
  transition: opacity .25s ease, scale .35s cubic-bezier(.3, .7, .3, 1.4);
}
.logo-hint.visible { opacity: 1; scale: 1; }
.logo-hint::after { content: ''; position: absolute; border: 6px solid transparent; }
.logo-hint--left { translate: 0 -50%; transform-origin: right center; }
.logo-hint--left::after { left: 100%; top: 50%; translate: -1px -50%; border-left-color: var(--ink); }
.logo-hint--below { transform-origin: 20px top; }
.logo-hint--below::after { bottom: 100%; left: 16px; translate: 0 1px; border-bottom-color: var(--ink); }

/* easter egg riddle card (script.js showRiddle) */
.egg-riddle {
  position: fixed;
  left: 50%;
  top: 50%;
  z-index: 320;
  width: min(460px, calc(100vw - 32px));
  padding: 22px 26px 24px;
  border-radius: 16px;
  background: var(--ink);
  color: var(--paper);
  box-shadow: 0 20px 60px rgba(0, 0, 0, .35);
  translate: -50% -50%;
  opacity: 0;
  transition: opacity .35s ease;
}
.egg-riddle.visible { opacity: 1; }
.egg-riddle-label {
  display: block;
  margin-bottom: 8px;
  color: #ffcc4d;
  font: 600 .64rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .14em;
}
.egg-riddle p { margin: 0; font: italic 400 1.12rem/1.7 var(--serif, Georgia), serif; }
.egg-riddle-close {
  position: absolute;
  top: 8px;
  right: 10px;
  border: 0;
  background: none;
  color: inherit;
  font-size: 1.2rem;
  opacity: .6;
  cursor: pointer;
}
.egg-riddle-close:hover { opacity: 1; }

/* --- phones: keep the page exactly screen-wide ---
   On a ~412px phone (e.g. Galaxy S21) the header's top row was wider than
   the screen, which made the whole page wider and let it slide sideways
   ("zoomed", right-hand buttons cut off). */
html, body { overflow-x: clip; } /* safety net: nothing may widen the page */
@media (max-width: 700px) {
  /* the ✉ icon beside the logo already does what "Get in touch" does */
  .header-contact { display: none; }
  /* cart back to a round icon */
  .cart-button:not(.dock-cart-button) { width: 30px; height: 30px; padding: 0; gap: 0; }
  .cart-button:not(.dock-cart-button) .cart-label { display: none; }
  .header-contact-group { gap: 8px; }
  /* the logo's hint bubble wraps and stays on screen */
  .logo-hint { white-space: normal; max-width: min(250px, calc(100vw - 24px)); }
}

/* add to cart: the flying bag and the cart's happy reaction (script.js) */
.cart-flyer {
  position: fixed;
  z-index: 80;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: #3964c3;
  color: #fff;
  box-shadow: 0 6px 18px rgba(57, 100, 195, .45);
  pointer-events: none;
}
.cart-flyer svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
@media (prefers-reduced-motion: no-preference) {
  .cart-button.cart-got { animation: cartGot .75s cubic-bezier(.3, .7, .3, 1.2); }
  .cart-button.cart-excited:not(.cart-got) { animation: cartExcited 1.5s ease-out infinite; }
}
@keyframes cartGot {
  0% { scale: 1; rotate: 0deg; }
  18% { scale: 1.4; rotate: -12deg; }
  36% { scale: .88; rotate: 9deg; }
  54% { scale: 1.15; rotate: -5deg; }
  72% { scale: .97; rotate: 2deg; }
  100% { scale: 1; rotate: 0deg; }
}
@keyframes cartExcited {
  0% { box-shadow: 0 0 0 0 rgba(57, 100, 195, .55); scale: 1; }
  20% { scale: 1.06; }
  45% { scale: 1; }
  100% { box-shadow: 0 0 0 12px rgba(57, 100, 195, 0); }
}

/* policy: "See an example" data request card (script.js toggles it) */
.data-example { margin: 22px 0 8px; }
.data-example-bar { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.data-example-bar p { flex: 1 1 260px; margin: 0; font-size: .9rem; color: var(--muted); }
.data-example-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 16px;
  border: 1px solid var(--wave-color, var(--acid));
  border-radius: 999px;
  background: color-mix(in srgb, var(--wave-color, var(--acid)) 12%, transparent);
  color: var(--ink);
  font: 600 .7rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .12em;
  cursor: pointer;
  transition: background .25s ease, color .25s ease;
}
.data-example-btn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.data-example-btn:hover, .data-example-btn[aria-expanded="true"] { background: var(--wave-color, var(--acid)); color: #fff; }
.data-example-card {
  position: relative;
  margin-top: 18px;
  padding: 22px 22px 18px;
  border: 1px dashed color-mix(in srgb, var(--wave-color, var(--acid)) 55%, var(--line));
  border-radius: 12px;
  background: color-mix(in srgb, var(--wave-color, var(--acid)) 5%, transparent);
  overflow: hidden;
}
.data-example-title { margin: 0 0 12px; font-size: .95rem; }
.data-example-card table { width: 100%; border-collapse: collapse; font-size: .84rem; line-height: 1.45; }
.data-example-card th, .data-example-card td { padding: 8px 10px 8px 0; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
.data-example-card th { width: 34%; color: var(--muted); font: 500 .66rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .08em; padding-top: 10px; }
.data-example-none { margin: 14px 0 4px; color: var(--wave-color, var(--acid)); }
.data-example-note { margin: 0; font-size: .8rem; color: var(--muted); }
.data-example-stamp {
  position: absolute;
  top: 14px;
  right: 14px;
  padding: 4px 10px;
  border: 2px solid #d0463b;
  border-radius: 6px;
  color: #d0463b;
  font: 800 .78rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .18em;
  rotate: -9deg;
  opacity: .85;
}
@media (prefers-reduced-motion: no-preference) {
  .data-example-btn.pressed { animation: exampleBtnPress .5s cubic-bezier(.3, .7, .3, 1.4); }
  .data-example-card.open { animation: exampleUnfold .45s cubic-bezier(.3, .7, .3, 1); transform-origin: top center; }
  .data-example-card.open tr { animation: exampleRow .45s cubic-bezier(.3, .7, .3, 1) both; animation-delay: calc(.15s + var(--r) * .05s); }
  .data-example-card.open .data-example-stamp { animation: stampInk .35s ease-out 1.05s both; }
  .data-example-card.open .stamp-tool { animation: stampTool 1.6s cubic-bezier(.4, 0, .3, 1) .45s both; }
  .data-example-card.open { animation: exampleUnfold .45s cubic-bezier(.3, .7, .3, 1), cardThud .3s ease-out 1.05s; }
}
@keyframes exampleBtnPress {
  0% { scale: 1; } 25% { scale: .88 1.06; } 55% { scale: 1.08 .95; } 100% { scale: 1; }
}
@keyframes exampleUnfold { from { opacity: 0; transform: scaleY(.85) translateY(-8px); } to { opacity: 1; transform: none; } }
@keyframes exampleRow { from { opacity: 0; translate: -14px 0; } to { opacity: 1; translate: 0 0; } }
@keyframes exampleStamp {
  0% { opacity: 0; scale: 2.2; rotate: -22deg; }
  60% { opacity: .9; scale: .92; rotate: -7deg; }
  100% { opacity: .85; scale: 1; rotate: -9deg; }
}
@media (max-width: 700px) {
  .data-example-card th { width: 40%; }
  .data-example-stamp { top: 10px; right: 10px; font-size: .66rem; }
}
/* jump buttons under the data example: glide to the part of the policy
   that explains each point (the arrival glow comes from the :target rule) */
.data-example-jumps { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.data-example-jump {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  border: 1px solid color-mix(in srgb, var(--wave-color, var(--acid)) 45%, var(--line));
  border-radius: 999px;
  color: var(--wave-color, var(--acid));
  font: 600 .64rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .1em;
  transition: background .2s ease, color .2s ease, translate .2s ease;
}
.data-example-jump:hover { background: var(--wave-color, var(--acid)); color: #fff; translate: 0 -2px; }
@media (prefers-reduced-motion: no-preference) {
  .data-example-card.open .data-example-jump { animation: exampleRow .45s cubic-bezier(.3, .7, .3, 1) both; }
  .data-example-card.open .data-example-jump:nth-child(1) { animation-delay: 1s; }
  .data-example-card.open .data-example-jump:nth-child(2) { animation-delay: 1.08s; }
  .data-example-card.open .data-example-jump:nth-child(3) { animation-delay: 1.16s; }
}

/* the rubber stamp that stamps EXAMPLE onto the data request card:
   swings in above the corner, slams down (squash + card thud), the ink mark
   appears under it, then it lifts off and flies away */
.data-example-card { overflow: visible; }
.stamp-tool {
  position: absolute;
  top: -32px; /* rubber lands right on the EXAMPLE mark */
  right: 30px;
  z-index: 3;
  width: 64px;
  height: 77px;
  opacity: 0;
  pointer-events: none;
  transform-origin: 50% 100%;
}
.stamp-tool svg { width: 100%; height: 100%; overflow: visible; filter: drop-shadow(0 6px 8px rgba(0, 0, 0, .25)); }
.st-knob { fill: #8a5a35; }
.st-neck { fill: #a8703f; }
.st-body { fill: #6b4426; }
.st-base { fill: #2c2c2a; }
.st-rubber { fill: #d0463b; }
@keyframes stampTool {
  0%   { opacity: 0; transform: translate(70px, -70px) rotate(28deg); }
  25%  { opacity: 1; transform: translate(0, -26px) rotate(-6deg); }
  38%  { transform: translate(0, -30px) rotate(0deg); }
  /* slam */
  44%  { transform: translate(0, 4px) scale(1.06, .82); }
  52%  { transform: translate(0, 2px) scale(.98, 1.03); }
  60%  { transform: translate(0, 2px) scale(1, 1); }
  /* lift off and fly away */
  78%  { opacity: 1; transform: translate(0, -40px) rotate(-4deg); }
  100% { opacity: 0; transform: translate(-60px, -110px) rotate(-24deg); }
}
@keyframes stampInk {
  0%   { opacity: 0; scale: 1.12; filter: blur(1px); }
  40%  { opacity: 1; scale: .97; filter: blur(0); }
  100% { opacity: .85; scale: 1; }
}
@keyframes cardThud {
  0%, 100% { translate: 0 0; }
  30% { translate: 0 3px; }
  60% { translate: 0 -1px; }
}
@media (prefers-reduced-motion: reduce) {
  .stamp-tool { display: none; }
}

/* policy cross-reference arrival glow when the site's own smooth scroll
   handles the jump (script.js animatedScrollTo) */
@media (prefers-reduced-motion: no-preference) {
  .xref-arrive { animation: xrefArrive 2.6s ease-out; border-radius: 8px; }
}
/* the browser's own jump (no-JS, reduced motion) also lands below the top */
.policy-section, .policy-hero { scroll-margin-top: 120px; }

/* closing the summary: the smiley wakes up a little (eyes pop open, a
   startled hop, a blink), then settles back into its happy smile */
@media (prefers-reduced-motion: no-preference) {
  .policy-changes.waking .sm-eyes-closed { animation: wakeClosed 1.2s ease-in-out; }
  .policy-changes.waking .sm-eyes-open { animation: wakeOpen 1.2s ease-in-out; }
  .policy-changes.waking .smiley { animation: wakeHop 1.2s ease-in-out; }
}
@keyframes wakeClosed { 0%, 100% { opacity: 1; } 8%, 78% { opacity: 0; } }
@keyframes wakeOpen {
  0%, 6% { opacity: 0; transform: scaleY(.1); }
  14% { opacity: 1; transform: scaleY(1.25); }
  22%, 40% { opacity: 1; transform: scaleY(1); }
  46% { transform: scaleY(.12); }
  52%, 72% { opacity: 1; transform: scaleY(1); }
  80%, 100% { opacity: 0; transform: scaleY(.1); }
}
@keyframes wakeHop {
  0%, 100% { transform: none; }
  10% { transform: translateY(-4px) scale(1.12); }
  20% { transform: translateY(0) rotate(-6deg); }
  30% { transform: rotate(6deg); }
  40% { transform: none; }
  85% { transform: rotate(-3deg) scale(1.04); }
}

/* egg 3 still locked (egg 2 not found yet): a shy little shrug */
@media (prefers-reduced-motion: no-preference) {
  .egg-smiley.shrug .smiley { animation: smileyShrug .6s ease-in-out; }
}
@keyframes smileyShrug {
  0%, 100% { transform: none; }
  25% { transform: translateY(1px) rotate(-8deg) scale(.95); }
  50% { transform: rotate(7deg) scale(.95); }
  75% { transform: rotate(-3deg); }
}

/* --- checkout safety messages + living faces (script.js faceSVG) --- */
.face { display: inline-flex; flex: none; width: 46px; height: 46px; }
.face svg { width: 100%; height: 100%; overflow: visible; }
.f-head { fill: #ffcc4d; }
.f-cheek { fill: #ff7892; opacity: .45; }
.f-eyes ellipse, .f-mouth-o { fill: #664500; }
.f-eyes-happy path, .f-mouth, .f-brows { fill: none; stroke: #664500; stroke-width: 2.2; stroke-linecap: round; }
.f-sweat { fill: #7cc4f0; }
.f-q { fill: #d0463b; font: 800 10px Manrope, Arial, sans-serif; }
.face svg * { transform-box: fill-box; transform-origin: center; }
@media (prefers-reduced-motion: no-preference) {
  .face--checking .f-eyes { animation: faceScan 1.4s ease-in-out infinite; }
  .face--checking svg { animation: faceNod 1.4s ease-in-out infinite; }
  .face--worried svg { animation: faceWobble 2.4s ease-in-out infinite; }
  .face--worried .f-sweat { animation: faceSweat 2.4s ease-in infinite; }
  .face--worried .f-eyes ellipse { animation: faceBlink 3s ease-in-out infinite; }
  .face--thinking svg { animation: faceTilt 2.6s ease-in-out infinite; }
  .face--thinking .f-eyes { animation: faceLookUp 2.6s ease-in-out infinite; }
  .face--thinking .f-q { animation: faceQ 2.6s ease-in-out infinite; }
  .face--happy svg { animation: faceHappy 1.6s cubic-bezier(.3, .7, .3, 1.3) 2; }
}
@keyframes faceScan { 0%, 100% { transform: translateX(-1.6px); } 50% { transform: translateX(1.6px); } }
@keyframes faceNod { 0%, 100% { transform: rotate(-3deg); } 50% { transform: rotate(3deg); } }
@keyframes faceWobble { 0%, 100% { transform: rotate(0); } 20% { transform: rotate(-5deg); } 40% { transform: rotate(4deg); } 60% { transform: rotate(-2deg); } }
@keyframes faceSweat { 0%, 30% { opacity: 0; transform: translateY(-2px); } 45% { opacity: 1; } 100% { opacity: 0; transform: translateY(7px); } }
@keyframes faceBlink { 0%, 44%, 52%, 100% { transform: scaleY(1); } 48% { transform: scaleY(.1); } }
@keyframes faceTilt { 0%, 100% { transform: rotate(-6deg); } 50% { transform: rotate(4deg); } }
@keyframes faceLookUp { 0%, 100% { transform: translate(1.4px, -1.6px); } 50% { transform: translate(-1px, -1.8px); } }
@keyframes faceQ { 0%, 100% { opacity: .5; transform: translateY(0) rotate(-8deg); } 50% { opacity: 1; transform: translateY(-2px) rotate(10deg); } }
@keyframes faceHappy { 0%, 100% { transform: none; } 20% { transform: translateY(-6px) scale(1.12); } 40% { transform: translateY(0) rotate(-8deg); } 60% { transform: rotate(8deg); } 80% { transform: rotate(-3deg); } }

.checkout-status {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 14px 0 4px;
  padding: 12px 14px;
  border-radius: 12px;
  font-size: .86rem;
  line-height: 1.5;
}
.checkout-status p { margin: 0; }
.checkout-status--checking { background: color-mix(in srgb, var(--acid) 10%, transparent); color: var(--muted); }
.checkout-status--problem { background: color-mix(in srgb, #d0463b 12%, transparent); border: 1px solid color-mix(in srgb, #d0463b 45%, transparent); color: var(--ink); }
.checkout-status--problem strong { color: #d0463b; }
@media (prefers-reduced-motion: no-preference) {
  .checkout-status--problem { animation: statusPop .5s cubic-bezier(.3, .7, .3, 1.4); }
}
@keyframes statusPop { from { opacity: 0; transform: scale(.94); } to { opacity: 1; transform: none; } }

.checkout-note {
  position: fixed;
  left: 50%;
  bottom: 24px;
  z-index: 330;
  display: flex;
  align-items: center;
  gap: 14px;
  width: min(460px, calc(100vw - 32px));
  padding: 16px 40px 16px 16px;
  border-radius: 14px;
  background: var(--ink);
  color: var(--paper);
  box-shadow: 0 14px 44px rgba(0, 0, 0, .3);
  translate: -50% 30px;
  opacity: 0;
  transition: opacity .35s ease, translate .45s cubic-bezier(.3, .7, .3, 1.3);
  font-size: .9rem;
  line-height: 1.5;
}
.checkout-note.visible { opacity: 1; translate: -50% 0; }
.checkout-note p { margin: 0; }
.checkout-note strong { color: #ffcc4d; }
.checkout-note-close { position: absolute; top: 8px; right: 10px; border: 0; background: none; color: inherit; font-size: 1.2rem; opacity: .6; cursor: pointer; }
#order-confirmation-modal .face { display: flex; margin: 0 0 6px; }

/* --- bonus egg: poke the policy summary smiley 5 times fast and it gets
   mad (red face, angry brows, frown, head shake "no"), then the bonus
   badge drops down. Not part of the numbered 1-5 hunt. (script.js) --- */
.sm-brows, .sm-frown { fill: none; stroke: #4a1a10; stroke-width: 2.4; stroke-linecap: round; opacity: 0; }
.sm-face { transition: fill .25s ease; }
.policy-changes summary { position: relative; }
.bonus-egg-badge {
  position: absolute;
  top: calc(100% + 10px);
  left: 0;
  z-index: 20;
  padding: 7px 13px;
  border-radius: 999px;
  background: var(--accent-surface, var(--ink));
  color: var(--on-accent-surface, var(--paper));
  font: 600 .66rem 'DM Mono', monospace;
  text-transform: none;
  letter-spacing: .02em;
  white-space: nowrap;
  opacity: 0;
  translate: 0 -6px;
  pointer-events: none;
  transition: opacity .25s ease, translate .35s cubic-bezier(.3, .7, .3, 1.4);
}
.policy-changes.bonus-shown .bonus-egg-badge { opacity: 1; translate: 0 0; }
.policy-changes.angry .sm-face { fill: #e0533f; }
.policy-changes.angry .sm-eyes-closed, .policy-changes.angry .sm-smile, .policy-changes.angry .sm-hmm, .policy-changes.angry .sm-cheek { opacity: 0; }
.policy-changes.angry .sm-eyes-open { opacity: 1; transform: scaleY(.8); }
.policy-changes.angry .sm-eye { fill: #4a1a10; }
.policy-changes.angry .sm-brows, .policy-changes.angry .sm-frown { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .policy-changes.angry .smiley { animation: headShakeNo 1.3s ease-in-out; }
}
@keyframes headShakeNo {
  0%, 100% { transform: none; }
  10% { transform: translateX(-3px) rotate(-14deg) scale(1.12); }
  22% { transform: translateX(3px) rotate(14deg) scale(1.12); }
  34% { transform: translateX(-3px) rotate(-12deg) scale(1.1); }
  46% { transform: translateX(3px) rotate(12deg) scale(1.1); }
  58% { transform: translateX(-2px) rotate(-8deg); }
  70% { transform: translateX(2px) rotate(6deg); }
  82% { transform: rotate(-2deg); }
}

/* --- the poke dialogue (after the bonus egg, every poke gets the next
   line; script.js). Moods: mad (red, angry brows), calm (yellow, eyes
   open), tired (half-closed eyes). The mouth moves while it "talks". --- */
.sm-talk { fill: #4a1a10; opacity: 0; transform-box: fill-box; transform-origin: center; }
.policy-changes.mood-mad .sm-face { fill: #e0533f; }
.policy-changes.mood-mad .sm-brows { opacity: 1; }
.policy-changes.mood-mad .sm-eye, .policy-changes.mood-calm .sm-eye, .policy-changes.mood-tired .sm-eye { fill: #4a1a10; }
.policy-changes:is(.mood-mad, .mood-calm, .mood-tired) :is(.sm-eyes-closed, .sm-smile, .sm-hmm, .sm-cheek) { opacity: 0; }
.policy-changes:is(.mood-mad, .mood-calm, .mood-tired) .sm-eyes-open { opacity: 1; transform: scaleY(.85); }
.policy-changes.mood-tired .sm-eyes-open { transform: scaleY(.35); }
.policy-changes:is(.mood-mad, .mood-calm, .mood-tired) .sm-frown { opacity: 1; }
.policy-changes.mood-calm .sm-frown { opacity: 0; }
.policy-changes.mood-calm .sm-hmm { opacity: 1; }
.policy-changes.talking .sm-frown, .policy-changes.talking .sm-hmm { opacity: 0 !important; }
.policy-changes.talking .sm-talk { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .policy-changes.talking .sm-talk { animation: mouthTalk .16s ease-in-out infinite alternate; }
}
@keyframes mouthTalk { from { transform: scaleY(.25); } to { transform: scaleY(1.1); } }
.poke-bubble {
  position: absolute;
  top: calc(100% + 12px);
  left: 0;
  z-index: 21;
  max-width: min(360px, 80vw);
  padding: 9px 14px;
  border-radius: 14px;
  background: var(--ink);
  color: var(--paper);
  font: 600 .82rem/1.45 Manrope, Arial, sans-serif;
  text-transform: none;
  letter-spacing: 0;
  white-space: normal;
  opacity: 0;
  translate: 0 -6px;
  scale: .9;
  transform-origin: 30px top;
  pointer-events: none;
  transition: opacity .2s ease, translate .3s cubic-bezier(.3, .7, .3, 1.4), scale .3s cubic-bezier(.3, .7, .3, 1.4);
}
/* anchored under the face (which sits at the end of the summary line), so
   the words come out of its mouth */
.poke-bubble { left: auto; right: -4px; transform-origin: calc(100% - 30px) top; }
.poke-bubble::before { content: ''; position: absolute; bottom: 100%; right: 24px; border: 7px solid transparent; border-bottom-color: var(--ink); }
.poke-bubble.visible { opacity: 1; translate: 0 0; scale: 1; }
.policy-changes.mood-mad .poke-bubble { background: #c8412f; color: #fff; }
.policy-changes.mood-mad .poke-bubble::before { border-bottom-color: #c8412f; }

/* --- the policy smiley's naps: every now and then it dozes off (sleepy
   lids, small mouth, breathing, floating z's), sleep-talks the hint
   "don't poke me fast... let me sleep", then wakes up smiling (script.js) --- */
.sm-sleep-eyes path { fill: none; stroke: #664500; stroke-width: 2.2; stroke-linecap: round; }
.sm-sleep-mouth { fill: #664500; }
.sm-sleep-eyes, .sm-sleep-mouth { opacity: 0; transition: opacity .5s ease; }
.sm-z { position: absolute; top: -4px; right: -6px; color: var(--muted); font: 700 .6rem Manrope, Arial, sans-serif; opacity: 0; pointer-events: none; }
.policy-changes.sleeping :is(.sm-eyes-closed, .sm-smile, .sm-cheek) { opacity: 0; transition: opacity .5s ease; }
.policy-changes.sleeping .sm-sleep-eyes, .policy-changes.sleeping .sm-sleep-mouth { opacity: 1; }
.policy-changes.sleeping.talking .sm-sleep-mouth { opacity: 0; }
@media (prefers-reduced-motion: no-preference) {
  .policy-changes.sleeping .smiley { animation: napBreathe 3.2s ease-in-out infinite; }
  .policy-changes.sleeping .sm-sleep-mouth { animation: napMouth 3.2s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
  .policy-changes.sleeping .sm-z { animation: napZ 3s ease-out infinite; }
  .policy-changes.sleeping .sm-z2 { animation-delay: 1s; }
  .policy-changes.sleeping .sm-z3 { animation-delay: 2s; font-size: .8rem; }
}
@keyframes napBreathe { 0%, 100% { transform: scale(1) rotate(-4deg); } 50% { transform: scale(1.07) rotate(-2deg); } }
@keyframes napMouth { 0%, 100% { transform: scale(.8); } 50% { transform: scale(1.2); } }
@keyframes napZ {
  0% { opacity: 0; translate: 0 0; scale: .6; }
  25% { opacity: .9; }
  100% { opacity: 0; translate: 10px -20px; scale: 1.2; }
}

/* --- free download box (script.js openFreeDownload) --- */
.free-price { color: #3f9c8c; }
.free-terms {
  max-height: 190px;
  overflow-y: auto;
  margin: 14px 0 6px;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: 10px;
  font-size: .86rem;
  line-height: 1.6;
}
.free-terms p { margin: 0 0 10px; }
.free-terms-end { color: var(--muted); font: 500 .62rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .12em; text-align: center; }
.free-scroll-hint { margin: 0 0 10px; color: var(--muted); font: 500 .66rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .1em; }
.free-scroll-hint.done { color: #3f9c8c; }
.free-agree.is-locked { opacity: .45; }
.free-step { overflow: hidden; max-height: 0; opacity: 0; transition: max-height .5s cubic-bezier(.3, .7, .3, 1), opacity .4s ease, margin .4s ease; }
.free-step.revealed { max-height: 200px; opacity: 1; margin-top: 12px; }
.free-email label { display: block; margin-bottom: 6px; font: 600 .66rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); }
.free-email-row { display: flex; gap: 8px; flex-wrap: wrap; }
.free-email-row input { flex: 1 1 200px; min-width: 0; padding: 11px 13px; border: 1px solid var(--line); border-radius: 8px; background: transparent; color: var(--ink); font: inherit; }
.free-email-row input:focus { outline: 2px solid #3f9c8c; outline-offset: 1px; }
.free-email-note { margin: 6px 0 0; color: var(--muted); font-size: .76rem; }
.free-result { display: flex; align-items: center; gap: 14px; margin-top: 14px; padding: 12px 14px; border-radius: 12px; background: color-mix(in srgb, var(--acid) 10%, transparent); font-size: .88rem; line-height: 1.5; }
.free-result p { margin: 0; }
.free-result--ok { background: color-mix(in srgb, #3f9c8c 14%, transparent); }
.free-result--problem { background: color-mix(in srgb, #d0463b 12%, transparent); }
.free-error { color: #d0463b; font-size: .85rem; }
/* these use display:flex, which would otherwise override [hidden] and leave
   an empty box showing */
.free-result[hidden], .checkout-status[hidden], .cart-free-checkout[hidden], .cart-checkout[hidden] { display: none !important; }

/* --- free pieces: second small "+ Add to cart" button, and the cart's
   paid / free groups --- */
.free-cart-button {
  position: absolute;
  right: 14px;
  bottom: 58px;
  padding: 7px 11px;
  border: 1px solid rgba(255, 255, 255, .7);
  border-radius: 0;
  background: rgba(0, 0, 0, .45);
  color: #fff;
  font: 600 .6rem Manrope, Arial, sans-serif;
  text-transform: uppercase;
  letter-spacing: .1em;
  cursor: pointer;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .25s, transform .25s, background .2s;
  backdrop-filter: blur(4px);
}
.free-cart-button:hover, .free-cart-button:focus-visible { background: rgba(0, 0, 0, .7); }
.free-cart-button.in-cart { background: #3f9c8c; border-color: #3f9c8c; }
.gallery-card:hover .free-cart-button, .gallery-card:focus-within .free-cart-button { opacity: 1; transform: none; }
@media (hover: none), (max-width: 700px) { .free-cart-button { opacity: 1; transform: none; } }
.cart-group-head { margin: 16px 0 6px; color: #3f9c8c; font: 600 .64rem 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .12em; }
.cart-free-checkout {
  display: block;
  width: 100%;
  margin-top: 10px;
  padding: 13px 16px;
  border: 1px solid #3f9c8c;
  background: color-mix(in srgb, #3f9c8c 14%, transparent);
  color: var(--ink);
  font: 600 .7rem 'DM Mono', monospace;
  text-transform: uppercase;
  letter-spacing: .12em;
  cursor: pointer;
}
.cart-free-checkout:hover { background: #3f9c8c; color: #fff; }
.free-list { margin: 4px 0 0; color: var(--muted); font-size: .82rem; }
