/* Skyland Cannabis Co. — visual language lifted from shop.skylandcannabis.com:
   cream canvas, raspberry #b33352 primary, dusty rose #d88e8e, Poppins + Roboto. */
:root {
  --cream: #f7f2ee;
  --panel: #ffffff;
  --ink: #2b2226;
  --muted: #8a7d82;
  --raspberry: #b33352;
  --raspberry-dark: #92283f;
  --rose: #d88e8e;
  --teal: #aed3d4;
  --violet: #b57ee8;
  --line: #e8dcd6;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: 'Roboto', system-ui, sans-serif;
  background: var(--cream);
  color: var(--ink);
  transition: background-color 1.4s ease; /* disco off, tier drops: buttery */
  /* footer always reaches the bottom edge — no dead strip below it
     (painfully visible in disco mode on short checkout pages) */
  min-height: 100vh; display: flex; flex-direction: column;
}
footer { margin-top: auto; }
h1, h2, h3, .btn, .brand { font-family: 'Poppins', system-ui, sans-serif; }
a { color: var(--raspberry); text-decoration: none; }
a:hover { opacity: .85; }

/* ---------- header ---------- */
.site-header, .ticker, footer { transition: background-color 1.4s ease; }
.site-header {
  display: flex; align-items: center; gap: 1.25rem;
  padding: .6rem 1.5rem; background: var(--cream);
  position: sticky; top: 0; z-index: 30;
  border-bottom: 1px solid var(--line);
  box-shadow: 0 2px 14px rgba(43, 34, 38, .08);
}
.brand img { height: 62px; display: block; }
.tabs { display: flex; gap: 1.1rem; flex: 1; flex-wrap: wrap; }
.tabs button {
  background: none; border: none; cursor: pointer;
  font-family: 'Poppins', sans-serif; font-weight: 500; font-style: italic;
  font-size: .95rem; color: var(--ink); padding: .35rem 0;
  border-bottom: 2px solid transparent;
}
.tabs button.active { color: var(--raspberry); border-bottom-color: var(--raspberry); }
.header-actions { display: flex; gap: .6rem; align-items: center; }

/* ---------- FIND US menu ----------
   Right-aligned by default because the button lives at the right end of the
   header: a left-aligned panel would hang off the edge of the viewport on every
   screen size. */
.findus { position: relative; }
.findus > .call-btn { display: inline-flex; align-items: center; gap: .4rem; }
.findus-caret { width: 10px; height: 7px; transition: transform .2s ease; }
.findus.open .findus-caret { transform: rotate(180deg); }
.findus-menu {
  position: absolute; top: calc(100% + .5rem); right: 0; z-index: 40;
  min-width: 268px; max-width: min(300px, calc(100vw - 1.4rem));
  background: #fff; border: 1px solid var(--line); border-radius: 14px;
  box-shadow: 0 18px 44px rgba(43, 34, 38, .18);
  padding: .4rem; overflow: hidden;
  /* Animated with transform and opacity only, and visibility rather than display,
     so it can actually transition — display:none jumps. */
  opacity: 0; visibility: hidden; transform: translateY(-6px) scale(.98);
  transform-origin: top right;
  transition: opacity .18s ease, transform .2s cubic-bezier(.2, 1, .3, 1), visibility .2s;
}
.findus.open .findus-menu { opacity: 1; visibility: visible; transform: none; }
.findus-item {
  display: flex; align-items: center; gap: .7rem;
  padding: .55rem .6rem; border-radius: 10px;
  color: var(--ink); text-decoration: none; font-size: .88rem;
  transition: background .14s ease, color .14s ease;
}
.findus-item:hover, .findus-item:focus-visible {
  background: #fbeef1; color: var(--raspberry); outline: none;
}
.findus-item svg { width: 20px; height: 20px; flex: 0 0 20px; color: var(--raspberry); }
.findus-item span { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.findus-item b { font-weight: 600; }
.findus-item em {
  font-style: normal; font-size: .74rem; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.findus-item:hover em, .findus-item:focus-visible em { color: var(--raspberry); opacity: .8; }
/* A hairline above the three contact rows, so socials and "how to reach us" read
   as two groups rather than one list of six. */
.findus-item.group-top { margin-top: .3rem; border-top: 1px solid var(--line); padding-top: .65rem; border-radius: 0 0 10px 10px; }
@media (prefers-reduced-motion: reduce) {
  .findus-menu, .findus-caret { transition: none; }
}
@media (max-width: 640px) {
  .findus-menu { min-width: 240px; }
  .findus-item em { font-size: .7rem; }
}
.btn {
  border: none; cursor: pointer; font-size: .92rem; font-weight: 600;
  padding: .55rem 1rem; border-radius: 8px; background: #efe5df; color: var(--ink);
}
.btn.pill {
  background: var(--raspberry); color: #fff; border-radius: 999px;
  padding: .6rem 1.3rem; font-style: italic;
}
.btn.pill:hover { background: var(--raspberry-dark); }
.btn.pill.light { background: #fff; color: var(--ink); font-style: normal; }
.btn.pill.wide { width: 100%; font-style: normal; margin-top: .5rem; }
.btn.sm { padding: .15rem .55rem; }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.cart-btn { background: var(--ink); color: #fff; }

/* ---------- announcement ticker ---------- */
.ticker { overflow: hidden; background: var(--cream); border-bottom: 1px solid var(--line); }
.ticker-track {
  display: flex; width: max-content; white-space: nowrap; padding: .55rem 0;
  animation: ticker 30s linear infinite;
}
.ticker-half {
  display: flex; align-items: center;
  font-family: 'Poppins', sans-serif; font-weight: 600; font-size: .95rem;
  letter-spacing: .06em;
}
/* Every span carries identical trailing space, so half #1 ends exactly where
   half #2 begins -> translateX(-50%) loops with zero visible seam or gap. */
.ticker-half > span { margin-right: 2.5rem; }
@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* ---------- hero carousel ---------- */
.hero {
  position: relative; overflow: hidden; color: #fff; text-align: center;
  height: clamp(340px, 46vw, 480px); background: #46323f;
}
.hero-slide {
  position: absolute; inset: 0; opacity: 0; transition: opacity .9s ease;
  cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.hero-slide.active { opacity: 1; z-index: 1; }
.hero-img {
  position: absolute; inset: 0; background-size: cover; background-position: center;
}
.hero-slide.active .hero-img { animation: kenburns 8s ease-out forwards; }
@keyframes kenburns { from { transform: scale(1); } to { transform: scale(1.09); } }
.hero-tint { position: absolute; inset: 0; }
.slide-elevated .hero-tint { background: linear-gradient(140deg, rgba(61,31,78,.82), rgba(179,51,82,.55)); }
.slide-grounded .hero-tint { background: linear-gradient(140deg, rgba(20,63,54,.85), rgba(28,110,94,.5)); }
.slide-gear .hero-tint { background: linear-gradient(140deg, rgba(64,42,30,.85), rgba(179,51,82,.45)); }
.hero-inner { position: relative; z-index: 2; padding: 1rem; }
.eyebrow { letter-spacing: .18em; font-size: .95rem; margin: 0 0 .8rem; }
.hero h1 {
  font-size: clamp(2.2rem, 6.5vw, 4.4rem); font-weight: 700; line-height: 1.06;
  margin: 0 0 1.5rem; letter-spacing: .04em;
}
/* staggered text entrance on the active slide */
.hero-slide .eyebrow, .hero-slide h1, .hero-slide .hero-cta {
  opacity: 0; transform: translateY(18px);
}
.hero-slide.active .eyebrow { animation: hero-in .7s ease .15s forwards; }
.hero-slide.active h1 { animation: hero-in .7s ease .3s forwards; }
.hero-slide.active .hero-cta { animation: hero-in .7s ease .5s forwards; display: inline-block; }
@keyframes hero-in { to { opacity: 1; transform: translateY(0); } }
.hero-arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 3;
  background: rgba(255,255,255,.16); color: #fff; border: none; cursor: pointer;
  width: 46px; height: 46px; border-radius: 50%; font-size: 1.7rem; line-height: 1;
  backdrop-filter: blur(4px); transition: background .2s;
}
.hero-arrow:hover { background: rgba(255,255,255,.35); }
.hero-arrow.prev { left: 14px; }
.hero-arrow.next { right: 14px; }
.hero-dots {
  position: absolute; bottom: 16px; left: 0; right: 0; z-index: 3;
  display: flex; justify-content: center; gap: .55rem;
}
.hero-dot {
  border: none; padding: 0; cursor: pointer; background: rgba(255,255,255,.35);
  width: 26px; height: 6px; border-radius: 999px; overflow: hidden; transition: width .3s;
}
.hero-dot.active { width: 52px; }
.hero-dot span { display: block; height: 100%; width: 0; background: #fff; }
.hero-dot.active span { animation: dot-progress var(--hero-interval, 6s) linear forwards; }
@keyframes dot-progress { to { width: 100%; } }

/* ---------- shop ---------- */
main { padding: 1.25rem 3rem 3rem; width: 100%; }
/* little status line under the search box (decency filter + VIP lookups) */
/* The search bar's little status line (decency filter, VIP lookup, promo codes).
   Absolutely positioned to the LEFT of the bar so it cannot participate in the
   .shop-tools flex row -- as a normal-flow sibling it used to claim 100% width
   and shove the input sideways the moment it appeared. */
.search-wrap { position: relative; display: block; }
.search-note {
  position: absolute; right: calc(100% + .6rem); top: 50%;
  transform: translateY(-50%);
  width: max-content; max-width: min(38vw, 300px); text-align: right;
  font-size: .74rem; color: var(--raspberry); font-weight: 600; line-height: 1.25;
  opacity: 0; transition: opacity .3s ease; pointer-events: none;
}
.search-note.show { opacity: 1; }
.search-shake { animation: searchShake .4s ease; }
@keyframes searchShake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-6px); }
  50% { transform: translateX(5px); }
  75% { transform: translateX(-3px); }
}
.shop-bar {
  display: flex; align-items: baseline; gap: 1.5rem; flex-wrap: wrap;
  padding: 1rem 0 1.25rem;
  position: sticky; top: var(--header-h, 82px); z-index: 25;
  transition: padding .25s ease;
}
/* once it sticks: a pretty frosted shelf floating under the header */
.shop-bar.floating {
  background: rgba(247, 242, 238, .88);
  backdrop-filter: blur(10px);
  box-shadow: 0 10px 24px rgba(43, 34, 38, .12);
  border-radius: 0 0 14px 14px;
  padding: .55rem .9rem .65rem;
  margin: 0 -0.9rem;
}
.shop-bar.floating #section-title { font-size: 1.1rem; }
#section-title #result-count { font-size: .45em; font-weight: 500; letter-spacing: .02em; color: var(--muted); text-transform: none; margin-left: .35rem; }
#section-title {
  color: var(--raspberry); font-size: clamp(1.5rem, 3.5vw, 2.2rem);
  letter-spacing: .05em; margin: 0; text-transform: uppercase;
}
.shop-tools { display: flex; align-items: center; gap: .9rem; flex: 1; justify-content: flex-end; }
#search {
  padding: .55rem .9rem; border: 1px solid var(--line); border-radius: 999px;
  background: #fff; font-size: .9rem;
  /* Sized to the placeholder, not guessed: "Search products, use codes, or dial
     in VIP Rewards..." is 53 characters, which at .9rem runs about 385px, plus
     .9rem of padding each side. 430px clears it with a little air so the text
     never sits flush against the pill's curve. */
  min-width: 430px; max-width: 100%;
}
/* If a viewport is narrow enough that even the shortened hint does not fit, fail
   as an ellipsis rather than a word chopped mid-letter. */
#search::placeholder { text-overflow: ellipsis; }
/* Tablets and phones: no usable gap beside the bar, so the note tucks under it
   -- still absolutely positioned, so the product grid never jumps. The wrap
   becomes the flex child and grows, which usually beats the 300px desktop
   minimum anyway. */
@media (max-width: 900px) {
  .search-wrap { flex: 1 1 240px; min-width: 0; }
  /* the long hint is swapped for a short one below this width (see fitSearchHint),
     so the bar no longer has to be wide enough for the full sentence */
  #search { min-width: 240px; }
  .search-note {
    right: auto; left: 0; top: calc(100% + .2rem);
    transform: none; text-align: left; max-width: 100%;
  }
}
.muted { color: var(--muted); font-size: .85rem; }
.shop-layout { display: flex; gap: 1.25rem; align-items: flex-start; }
.grid {
  flex: 1; display: grid; gap: 0; /* touching squares, like the original site */
  /* Fixed counts, fluid card size: the grid always fills the viewport and
     cards scale up with the monitor -- matching shop.skylandcannabis.com */
  grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 1100px) {
  .grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 860px) {
  .grid { grid-template-columns: repeat(2, 1fr); }
}
.card {
  background: var(--panel); border-radius: 0; overflow: hidden;
  display: flex; flex-direction: column; position: relative; z-index: 1;
  transition: box-shadow .25s ease, z-index 0s;
}
.card:hover {
  z-index: 2;
  box-shadow: 0 10px 30px rgba(43,34,38,.22);
}
.card .imgwrap { overflow: hidden; }
.card .imgwrap img { transition: transform .45s ease; }
.card:hover .imgwrap img { transform: scale(1.06); }
.card .imgwrap { aspect-ratio: 1; background: #efe7e2; }
.card img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card .noimg {
  width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
  color: var(--rose); font-family: 'Poppins', sans-serif; font-weight: 600; font-size: .85rem;
  background: linear-gradient(135deg, #f3e4e0, #ecd3d3);
  text-align: center; padding: 1rem;
}
.card .body { padding: .8rem .9rem 1rem; display: flex; flex-direction: column; gap: .3rem; flex: 1; }
.card h3 {
  margin: 0; font-size: .95rem; font-weight: 600; color: var(--raspberry); line-height: 1.3;
}
.variant { color: var(--muted); font-size: .8rem; margin: 0; }
.price { font-weight: 700; margin: .1rem 0 0; font-size: 1rem; }
.stock-note { font-size: .78rem; color: var(--muted); margin: 0; min-height: 1em; }
/* ---------- the add-to-cart quantity counter ----------
   The counter sits BEFORE the button in the DOM and expands from zero width, so
   growing it slides the button along rather than covering it. Width rather than a
   transform, because a transform would leave the button's hit area behind where
   it was drawn. */
.add-row { margin-top: auto; display: flex; align-items: stretch; }
.add-row .btn.add { margin-top: 0; flex: 1 1 auto; }
.qty-nudge {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
  width: 0; margin-right: 0; opacity: 0; overflow: hidden; pointer-events: none;
  border: 1px solid var(--line); border-radius: 999px; background: #fff;
  /* COLLAPSING uses this rule: opacity has no delay, so the counter fades out
     before the gap closes. */
  transition: width .3s cubic-bezier(.2, .9, .3, 1),
              margin-right .3s cubic-bezier(.2, .9, .3, 1),
              opacity .18s ease;
}
.card.nudging .qty-nudge {
  width: 96px; margin-right: .45rem; opacity: 1; pointer-events: auto;
  /* EXPANDING: the opacity delay lets the space open first and the counter fade
     into it. Fading in while the gap is still growing reads as a glitch. */
  transition: width .3s cubic-bezier(.2, .9, .3, 1),
              margin-right .3s cubic-bezier(.2, .9, .3, 1),
              opacity .22s ease .13s;
}
.nudge-step {
  width: 30px; padding: 0; border: none; background: none; cursor: pointer;
  font-size: 1rem; line-height: 1; color: var(--raspberry);
}
.nudge-step:disabled { opacity: .3; cursor: default; }
.nudge-n {
  font-size: .9rem; min-width: 18px; text-align: center;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
/* The running subtotal under the buy row. Height is animated rather than toggled with
   display, so the card does not jump the instant the counter opens — and it is only in
   the layout while the counter is out, so a grid of cards is not permanently taller for
   a line nobody is reading. */
.nudge-hint {
  margin: 0; overflow: hidden; max-height: 0; opacity: 0;
  font-size: .74rem; line-height: 1.35; color: var(--muted);
  transition: max-height .28s cubic-bezier(.2, .9, .3, 1), opacity .2s ease, margin-top .28s ease;
}
.card.nudging .nudge-hint { max-height: 2.4rem; opacity: 1; margin-top: .35rem; }
.nudge-hint b { color: var(--ink); font-variant-numeric: tabular-nums; }
.nudge-hint.nudge-hint-won { color: var(--raspberry); }
.nudge-hint.nudge-hint-won b { color: var(--raspberry); }
@media (prefers-reduced-motion: reduce) { .nudge-hint { transition: none; } }
@media (prefers-reduced-motion: reduce) {
  .qty-nudge, .card.nudging .qty-nudge { transition: none; }
}

.card .btn.add {
  margin-top: auto; background: var(--raspberry); color: #fff; border-radius: 999px;
}
.card .btn.add:hover { background: var(--raspberry-dark); }
.card .btn.add:disabled { background: #cbb9b3; }

/* ---------- cart / checkout ---------- */
#cart-panel {
  position: fixed; top: 0; right: 0; height: 100dvh; z-index: 70;
  width: min(400px, 94vw); background: var(--panel); padding: 1.1rem;
  border-left: 1px solid var(--line); box-shadow: -12px 0 34px rgba(43,34,38,.25);
  overflow-y: auto; transform: translateX(105%);
  transition: transform .25s ease;
}
#cart-panel.open { transform: translateX(0); }
#cart-backdrop {
  position: fixed; inset: 0; background: rgba(43,34,38,.45); z-index: 69;
}
.cart-head { display: flex; align-items: center; justify-content: space-between; }
#cart-close { font-size: 1rem; line-height: 1; }
body.cart-open { overflow: hidden; }
#cart-panel h2 { margin: 0 0 .5rem; color: var(--raspberry); font-size: 1.2rem; }
.cart-line { display: flex; gap: .5rem; align-items: center; padding: .3rem 0; font-size: .9rem; }
.cart-line .nm { flex: 1; }
#quote { padding: .6rem 0; color: var(--muted); font-size: .9rem; border-top: 1px dashed var(--line); margin-top: .4rem; }
form h3 { margin: .9rem 0 .3rem; font-size: .95rem; }
form input, form select {
  width: 100%; margin: .22rem 0; padding: .55rem .7rem; border-radius: 8px;
  border: 1px solid var(--line); background: #fdfbfa; color: var(--ink); font-size: .9rem;
}
form .row { display: flex; gap: .5rem; }
.check { display: flex; gap: .5rem; font-size: .85rem; margin: .5rem 0; color: var(--muted); align-items: flex-start; }
.check input { width: auto; margin-top: .15rem; }
#checkout-msg.ok { color: #1c7a4b; font-weight: 600; }
#checkout-msg.err { color: var(--raspberry-dark); font-weight: 600; }
.hidden { display: none !important; }

/* Honeypot: visually removed but present in the DOM for bots */
.hp-wrap { position: absolute; left: -9999px; top: -9999px; height: 1px; width: 1px; overflow: hidden; }
#turnstile-box { margin: .5rem 0; min-height: 0; }

/* ---------- age gate: midnight garden ---------- */
.gate-backdrop {
  position: fixed; inset: 0; z-index: 50;
  background: radial-gradient(1200px 760px at 50% 32%, #40292a 0%, #23141a 55%, #0d0709 100%);
  display: flex; align-items: center; justify-content: center; color: var(--cream);
  overflow: hidden;
}
.gate { text-align: center; width: min(600px, 94vw); padding: 1rem; position: relative; z-index: 3; }
.gate-frame {
  position: relative; padding: 3rem 2.4rem 2.2rem;
  border: 1px solid rgba(247, 242, 238, .18);
  background: rgba(28, 15, 18, .55);
  box-shadow: 0 34px 90px rgba(0, 0, 0, .55);
  backdrop-filter: blur(7px);
}
.gate-kicker {
  font-family: 'Poppins', sans-serif; font-size: .66rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: .38em; color: var(--rose); margin: 0 0 1.2rem;
}
.gate-title {
  font-family: 'Poppins', sans-serif; font-weight: 600;
  font-size: clamp(1.7rem, 4.6vw, 2.4rem); letter-spacing: .08em;
  text-transform: uppercase; line-height: 1.15; margin: 0 0 1.2rem; color: var(--cream);
  text-shadow: 0 2px 26px rgba(216, 142, 142, .3);
}
.gate-rule {
  width: 72px; height: 1px; margin: 0 auto 1.3rem;
  background: linear-gradient(90deg, transparent, rgba(247, 242, 238, .6), transparent);
}
.gate-query {
  font-family: 'Poppins', sans-serif; font-weight: 300; font-size: .95rem;
  color: #e3d5e4; letter-spacing: .02em; margin: 0;
}
.gate .row { display: flex; gap: 1rem; justify-content: center; margin-top: 1.8rem; }
.gate-yes, .gate-no {
  font-family: 'Poppins', sans-serif; font-weight: 500; font-size: .78rem;
  text-transform: uppercase; letter-spacing: .2em;
  padding: .9rem 1.9rem; border-radius: 2px; transition: all .2s ease;
}
.gate-yes { background: var(--cream); color: var(--ink); border: 1px solid var(--cream); }
.gate-yes:hover { background: transparent; color: var(--cream); box-shadow: 0 0 26px rgba(247, 242, 238, .25); }
.gate-no { background: transparent; color: var(--rose); border: 1px solid rgba(216, 142, 142, .5); }
.gate-no:hover { border-color: var(--rose); color: var(--cream); }
/* the under-21 sequence: a button loses its screws, swings, and plummets */
.gate-yes.gate-no-falling,
.gate-no.gate-no-falling {
  pointer-events: none;
  transform-origin: top right; /* one screw holds… briefly */
  animation: gateNoFall 1.7s cubic-bezier(.55, 0, .8, .4) forwards;
}
@keyframes gateNoFall {
  0% { transform: rotate(0deg); }
  14% { transform: rotate(14deg); }            /* first screw pops */
  26% { transform: rotate(9deg); }             /* wobble */
  38% { transform: rotate(22deg); }            /* hanging by a thread */
  48% { transform: rotate(19deg); }
  100% { transform: translateY(130vh) rotate(140deg); opacity: .9; } /* gravity wins */
}
/* the fade to black rides its OWN layer: replacing the gradient backdrop
   mid-transition made the gate briefly see-through — never again */
.gate-cinematic::after {
  content: ''; position: absolute; inset: 0; z-index: 2;
  background: #050203; opacity: 0;
  animation: gateFadeBlack 3s ease forwards;
}
@keyframes gateFadeBlack { to { opacity: 1; } }
.gate-cinematic .gate-frame,
.gate-cinematic .gate-leaves {
  opacity: 0; transition: opacity 2.8s ease;
  pointer-events: none;
}
.gate-21 {
  position: absolute; inset: 0; z-index: 3; display: flex; align-items: center; justify-content: center;
  font-family: 'Poppins', sans-serif; font-weight: 700;
  font-size: clamp(5rem, 22vw, 17rem); letter-spacing: .08em;
  color: var(--cream); opacity: 0; transform: scale(.9);
  text-shadow: 0 0 90px rgba(216, 142, 142, .45), 0 0 30px rgba(247, 242, 238, .25);
  /* no transition-delay: app.js does not create this element until the black
     overlay has finished, so any delay here would just add dead time */
  transition: opacity 2.6s ease, transform 5.4s cubic-bezier(.2, .6, .2, 1);
}
.gate-21-show .gate-21 { opacity: 1; transform: scale(1); }
/* returning to a tab that already answered: straight to the verdict */
.gate-instant::after { animation-duration: .01s; }
.gate-instant .gate-frame, .gate-instant .gate-leaves { transition-duration: .01s; }
.gate-instant .gate-21 { transition-duration: .8s, .8s; }
.gate-foot {
  font-family: 'Poppins', sans-serif; font-weight: 300; font-size: .66rem;
  letter-spacing: .14em; color: #9c86a8; margin: 1.6rem 0 0;
}

/* falling leaves: 3D tumble + trippy pulse + ghost trails */
.gate-leaves {
  position: absolute; inset: 0; overflow: hidden; z-index: 1;
  pointer-events: none; perspective: 900px;
}
.leaf-fall {
  position: absolute; top: -120px; left: var(--x, 50%);
  animation: leafFall var(--dur, 9s) linear var(--delay, 0s) infinite;
  will-change: transform;
}
.leaf-fall.ghost {
  opacity: var(--ghost-op, .28);
  filter: blur(var(--ghost-blur, 3px)) saturate(1.8);
}
/* the trail copies pulse on their own rhythm — a luminous wake */
.leaf-fall.ghost svg {
  animation: trailPulse var(--pulse, 2.4s) ease-in-out var(--delay, 0s) infinite alternate;
}
@keyframes trailPulse {
  from { opacity: .35; transform: scale(.8); filter: drop-shadow(0 0 6px var(--glow, #b57ee8)); }
  to { opacity: .9; transform: scale(1.3); filter: drop-shadow(0 0 26px var(--glow, #b57ee8)); }
}
.leaf-sway {
  display: block;
  animation: leafSway var(--sway, 3.2s) ease-in-out var(--delay, 0s) infinite alternate;
}
.leaf-spin {
  display: block; transform-style: preserve-3d;
  animation: leafSpin var(--spin, 3s) linear var(--delay, 0s) infinite;
}
.leaf-spin svg {
  display: block; width: var(--size, 44px); height: var(--size, 44px);
  animation: leafPulse var(--pulse, 2.4s) ease-in-out var(--delay, 0s) infinite alternate;
}
@keyframes leafPulse {
  from {
    opacity: .7; transform: scale(.88);
    filter: drop-shadow(0 0 8px var(--glow, #b57ee8)) hue-rotate(-12deg) saturate(.95);
  }
  to {
    opacity: 1; transform: scale(1.16);
    filter: drop-shadow(0 0 30px var(--glow, #b57ee8)) hue-rotate(12deg) saturate(1.6);
  }
}
@keyframes leafFall {
  from { transform: translateY(-12vh); }
  to { transform: translateY(114vh); }
}
@keyframes leafSway {
  from { transform: translateX(calc(var(--amp, 28px) * -1)) rotate(-14deg); }
  to { transform: translateX(var(--amp, 28px)) rotate(14deg); }
}
@keyframes leafSpin {
  from { transform: rotateX(0deg) rotateY(0deg); }
  to { transform: rotateX(var(--rx, 360deg)) rotateY(var(--ry, 720deg)); }
}
@media (prefers-reduced-motion: reduce) {
  .gate-leaves { display: none; }
}

/* ---------- footer / fireworks ---------- */
footer {
  padding: 1.6rem; color: var(--muted); font-size: .8rem;
  border-top: 1px solid var(--line); text-align: center; background: #f2e9e4;
}
#fireworks { position: fixed; inset: 0; pointer-events: none; z-index: 100; display: none; }

@media (max-width: 900px) {
  .shop-layout { flex-direction: column; }
  .brand img { height: 48px; }
}

/* ---------- phones ---------- */
@media (max-width: 640px) {
  .site-header { flex-wrap: wrap; gap: .5rem .75rem; padding: .5rem .8rem; }
  .brand img { height: 40px; }
  .header-actions { margin-left: auto; gap: .4rem; }
  .btn { padding: .5rem .8rem; font-size: .85rem; }
  .btn.pill { padding: .55rem 1rem; }
  .btn.exp-switch { font-size: .8rem; }

  .ticker-half { font-size: .78rem; }
  .ticker-half > span { margin-right: 1.4rem; }

  .hero-inner { padding: 3.2rem .9rem 3.6rem; }
  .exp-choice { padding: 2rem .9rem 2.4rem; }
  .exp-choice .exp-cards { grid-template-columns: 1fr 1fr; gap: .6rem; } /* side-by-side, app-like */
  .exp-card { padding: 1.2rem .7rem 1rem; }
  .exp-name { font-size: 1.15rem; letter-spacing: .1em; }
  .exp-icon svg { width: 40px; height: 40px; }
  .exp-desc { display: none; } /* tiles, not essays, on a phone */
  .exp-tag { font-size: .62rem; }
  .exp-cta { font-size: .72rem; padding: .45rem .8rem; }
  .exp-choice h1 { font-size: 1.7rem; }
  .exp-sub { font-size: .9rem; }
  .exp-accessories { padding: .9rem 1rem; font-size: .9rem; width: calc(100vw - 1.8rem); }
  .muzak-chips { gap: .4rem; }
  .muzak-chip { font-size: .72rem; padding: .45rem .85rem; }

  .cat-section { padding: 1.2rem .8rem 0; }
  .cat-tile { width: 112px; }
  .tile-icon svg { width: 34px; height: 34px; }
  .tile-label { font-size: .68rem; }
  .cat-tile.all-tile .all-label { font-size: .95rem; }

  main { padding: .8rem .8rem 2rem; }
  .shop-bar { flex-direction: column; align-items: stretch; gap: .6rem; padding: .6rem 0 .9rem; }
  .shop-tools { justify-content: space-between; }
  #search { width: 100%; min-width: 0; }
  .grid { grid-template-columns: repeat(2, 1fr); gap: 0; } /* phones: 2 across, always */
  .card h3 { font-size: .84rem; }
  .card .body { padding: .55rem .6rem .7rem; }
  .price { font-size: .92rem; }
  .card .btn.add { padding: .55rem .5rem; font-size: .82rem; }
  /* MOBILE AUDIT FIX: 16px, not .78rem. iOS Safari auto-zooms the page when any
     focused form control is under 16px, so tapping the variation dropdown lurched the
     whole grid sideways and left the customer pinch-zooming back out. 16px is the
     documented threshold; the visual size loss on the card is real but small, and a
     dropdown that jolts the page is worse than one a shade larger. */
  .variation-select { font-size: 16px; }

  .checkout-main { padding: .8rem; }
  .checkout-card { padding: 1rem; }
  .site-header.slim { padding: .5rem .8rem; }
  .secure-note { font-size: .85rem; }
  .back-link { font-size: .8rem; }
  form input, form select, #search { font-size: 16px; } /* prevents iOS zoom-on-focus */
  .extras-row { flex-wrap: wrap; gap: .4rem 1.2rem !important; }
  .receipt { padding: .8rem .9rem; }
  .gate-title { font-size: 1.35rem; }
  .gate .row { flex-direction: column; }
  footer { padding: 1.1rem .9rem; font-size: .72rem; }
}

/* comfortable touch targets on any coarse pointer (phones/tablets) */
@media (pointer: coarse) {
  .btn, .cart-line .inc, .cart-line .dec { min-height: 42px; }
  .tabs button, .linklike { min-height: 40px; }
}

/* ---------- category tiles (main section) ---------- */
.cat-section { padding: 2rem 3rem 0; width: 100%; }
.cat-heading {
  color: var(--raspberry); font-size: clamp(1.4rem, 3vw, 2rem);
  letter-spacing: .05em; margin: 0 0 1.2rem; text-transform: uppercase;
  font-family: 'Poppins', sans-serif; text-align: center;
}
.cat-tiles { display: flex; flex-wrap: wrap; justify-content: center; gap: 0; }
.cat-tile {
  border: none; padding: 0; cursor: pointer; position: relative; overflow: hidden;
  width: 148px; aspect-ratio: 1; border-radius: 0;
  background: linear-gradient(135deg, #2b1022 0%, #5a2440 58%, #8a3d5c 140%);
  color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .55rem; text-align: center;
  transition: filter .15s ease, box-shadow .15s ease;
}
.cat-tile:hover { filter: brightness(1.15); z-index: 1; box-shadow: 0 6px 18px rgba(43, 16, 34, .45); }
/* rail fx v4: still only the ACTIVE tile moves, but with real contrast. v3's
   brightness(1..1.18) was too subtle to read at a glance -- the pulse now
   carries saturation and a glow that breathes with it, and the cream outline
   brightens on the same beat so the whole tile reads as "you are here". */
.cat-tile.active {
  outline: 3px solid var(--cream); outline-offset: -3px;
  animation: activePulse 2.6s ease-in-out infinite;
  z-index: 2; /* the glow should sit over its neighbours, not under them */
}
@keyframes activePulse {
  0%, 100% {
    filter: brightness(1) saturate(1);
    box-shadow: 0 0 0 rgba(247, 242, 238, 0), inset 0 0 0 rgba(247, 242, 238, 0);
    outline-color: var(--cream);
  }
  50% {
    filter: brightness(1.34) saturate(1.28);
    box-shadow: 0 0 22px rgba(247, 242, 238, .5), inset 0 0 26px rgba(247, 242, 238, .22);
    outline-color: #fffdfb;
  }
}
@media (prefers-reduced-motion: reduce) {
  /* no pulse, but keep the contrast so the active tile is still obvious */
  .cat-tile.active {
    animation: none;
    filter: brightness(1.2) saturate(1.18);
    box-shadow: 0 0 18px rgba(247, 242, 238, .4);
  }
}
/* desktop: shrink the rail to its tiles so the sweep never floats in space */
@media (min-width: 1025px) {
  .cat-tiles { width: max-content; max-width: 100%; margin: 0 auto; }
}
@keyframes all-sheen {
  0%, 55% { transform: translateX(-70%); }
  85%, 100% { transform: translateX(70%); }
}
.tile-icon { display: block; line-height: 0; }
.tile-icon svg { width: 42px; height: 42px; filter: drop-shadow(0 2px 6px rgba(0, 0, 0, .35)); }
.tile-label {
  font-family: 'Poppins', sans-serif; font-weight: 600; font-size: .78rem;
  letter-spacing: .07em; text-transform: uppercase; color: #fff; padding: 0 .35rem;
}
.cat-tile.all-tile {
  background: linear-gradient(135deg, #5a1226 0%, var(--raspberry) 60%, var(--rose) 100%);
}
.cat-tile.all-tile .all-label {
  color: #fff; font-family: 'Poppins', sans-serif; font-weight: 700;
  font-size: 1.3rem; letter-spacing: .38em; font-style: italic;
  padding-left: .38em; /* optically centers the letter-spaced text */
  text-transform: uppercase; z-index: 1;
}
.cat-tile.all-tile:hover { box-shadow: 0 8px 22px rgba(179, 51, 82, .4); }
/* navigation tile: back to the experience chooser */
.cat-tile.switch-tile {
  background: linear-gradient(135deg, #123f3a 0%, #2e7d6e 60%, #7fc4b2 140%);
  color: #fff;
}
.cat-tile.switch-tile .tile-label { color: #fff; }
.cat-tile.switch-tile:hover { box-shadow: 0 8px 22px rgba(18, 63, 58, .45); }

.cart-freeship {
  margin-top: .45rem; font-family: 'Poppins', sans-serif; font-weight: 700;
  font-size: .72rem; letter-spacing: .12em; color: #1c8a4f;
}
.egg-freeship.freeship-lost { color: #ff8896; text-shadow: 0 0 46px rgba(255, 90, 110, .7); }

/* ---------- screensaver (desktop only) ---------- */
.screensaver {
  position: fixed; inset: 0; z-index: 95; overflow: hidden;
  background: #0d060b; cursor: pointer;
  opacity: 0; transition: opacity 1.2s ease;
  /* AN UNREVEALED SCREENSAVER MUST NOT EAT THE PAGE.
     opacity:0 hides an element; it does not stop it receiving clicks. This is fixed and
     inset:0, so between being appended and being given .show it is an invisible sheet over the
     whole viewport -- and when a throw once left it stuck in that state, every click and every
     attempt to focus the search box landed on it instead. The site looked entirely fine and
     did nothing at all: no eggs, no logo, no add-to-cart.
     Inert until revealed turns that failure from "the shop is dead" into "there is a spare
     div in the DOM". */
  pointer-events: none;
}
.screensaver.show { opacity: 1; pointer-events: auto; }
.ss-blob {
  position: absolute; width: 58vmax; height: 58vmax; will-change: transform, border-radius;
  filter: blur(50px) saturate(1.3); opacity: .55; mix-blend-mode: screen;
  animation:
    ssMorph var(--m, 17s) ease-in-out infinite alternate,
    ssDrift var(--t, 33s) ease-in-out infinite alternate,
    ssPulse var(--p, 7s) ease-in-out infinite alternate;
}
.ss-blob.b1 { left: -12vmax; top: -10vmax; background: radial-gradient(circle at 40% 40%, #b33352, transparent 65%); }
.ss-blob.b2 { right: -14vmax; top: 12vmax; background: radial-gradient(circle at 60% 40%, #8a3d5c, transparent 65%); }
.ss-blob.b3 { left: 18vmax; bottom: -16vmax; background: radial-gradient(circle at 50% 60%, #c98a5e, transparent 65%); }
.ss-blob.b4 { right: 8vmax; bottom: -8vmax; background: radial-gradient(circle at 50% 50%, #d88e8e, transparent 62%); }
@keyframes ssMorph {
  0% { border-radius: 62% 38% 55% 45% / 48% 60% 40% 52%; }
  50% { border-radius: 38% 62% 42% 58% / 60% 38% 62% 40%; }
  100% { border-radius: 55% 45% 60% 40% / 42% 55% 45% 58%; }
}
@keyframes ssDrift {
  to { transform: translate(9vmax, -7vmax) rotate(38deg); }
}
@keyframes ssPulse {
  from { opacity: .3; scale: .82; }
  to { opacity: .7; scale: 1.14; }
}
.ss-text {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 2rem;
  font-family: 'Poppins', sans-serif; font-weight: 700;
  font-size: clamp(1.8rem, 5vw, 4rem); letter-spacing: .3em; line-height: 1.5;
  /* Above the falling logos (z-index 1/3) and the trail (DOM-appended after this, and so
     painted over it by default) -- mix-blend-mode only reacts to whatever is actually
     BEHIND it, so the text has to be the topmost layer for this to see the logos at all. */
  z-index: 8;
  /* PURE WHITE, DIFFERENCE-BLENDED, NOT A FIXED COLOUR. |white - backdrop| turns the text
     black wherever a light shape (the logo artwork is a pale badge on transparent, ghosting
     through at low opacity as it falls) sits behind it, and keeps it reading light against
     the dark screensaver background everywhere else -- automatically, every frame, with
     zero collision logic: the compositor recomputes the blend continuously as the animated
     logos and their trail move underneath, so it holds up under any of it without this ever
     needing to know where a logo actually is. */
  color: #fff; mix-blend-mode: difference;
  text-shadow: 0 0 60px rgba(216, 142, 142, .8);
  animation: ssTextPulse 5s ease-in-out infinite alternate;
}
@keyframes ssTextPulse {
  from { opacity: .75; letter-spacing: .3em; }
  to { opacity: 1; letter-spacing: .36em; }
}
/* floating music controls: tweak the radio without waking the page */
.ss-controls {
  position: absolute; left: 50%; bottom: 7vh; transform: translateX(-50%);
  /* Above the falling logos (z-index 1/3) and their trail (unset, but DOM-appended after
     this and so painted over it by default) -- a real, clickable control has to win that
     fight every time, not just when the DOM order happens to favour it. */
  z-index: 10;
  display: flex; align-items: center; gap: .9rem;
  padding: .65rem 1.2rem; border-radius: 999px;
  background: rgba(43, 34, 38, .55); backdrop-filter: blur(10px);
  border: 1px solid rgba(247, 242, 238, .22);
  cursor: default; animation: ssCtlDrift 9s ease-in-out infinite alternate;
}
@keyframes ssCtlDrift {
  from { transform: translateX(-50%) translateY(0); }
  to { transform: translateX(-50%) translateY(-14px); }
}
/* the genre label is a station picker; the menu opens UPWARD because the
   controls float near the bottom of the screen */
.ss-picker { position: relative; }
.ss-genre {
  appearance: none; background: transparent; border: none; padding: 0;
  display: inline-flex; align-items: center; gap: .35rem; cursor: pointer;
  font-family: 'Poppins', sans-serif; font-weight: 600; font-size: .78rem;
  letter-spacing: .22em; text-transform: uppercase; color: var(--cream);
  white-space: nowrap;
}
.ss-genre::after {
  content: ''; width: 0; height: 0;
  border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-top: 5px solid rgba(247, 242, 238, .75);
  transition: transform .25s ease;
}
.ss-picker.open .ss-genre::after { transform: rotate(180deg); }
.ss-menu {
  position: absolute; left: 50%; bottom: calc(100% + .55rem);
  transform: translateX(-50%) translateY(6px);
  min-width: 176px; max-height: 44vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: .1rem;
  padding: .35rem; border-radius: 14px;
  background: rgba(30, 22, 26, .9); backdrop-filter: blur(12px);
  border: 1px solid rgba(247, 242, 238, .22);
  box-shadow: 0 -14px 34px rgba(13, 6, 11, .5);
  opacity: 0; pointer-events: none;
  transition: opacity .28s ease, transform .28s ease;
}
.ss-picker.open .ss-menu {
  opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0);
}
.ss-menu-item {
  appearance: none; background: transparent; border: none; cursor: pointer;
  text-align: left; padding: .45rem .6rem; border-radius: 9px;
  font-family: 'Poppins', sans-serif; font-weight: 600;
  font-size: .72rem; letter-spacing: .16em; text-transform: uppercase;
  color: rgba(247, 242, 238, .88);
}
.ss-menu-item:hover { background: rgba(247, 242, 238, .14); color: #fff; }
.ss-menu-empty {
  padding: .45rem .6rem; font-size: .68rem; letter-spacing: .1em;
  color: rgba(247, 242, 238, .6);
}
.ss-mute {
  border: 1px solid rgba(247, 242, 238, .4); background: transparent; color: var(--cream);
  font-family: 'Poppins', sans-serif; font-size: .66rem; font-weight: 600;
  letter-spacing: .18em; padding: .3rem .7rem; border-radius: 999px; cursor: pointer;
}
.ss-mute:hover { background: rgba(247, 242, 238, .15); }
.ss-vol { width: 130px; accent-color: #d88e8e; cursor: pointer; }

/* ---------- search easter eggs ---------- */
html.egg-clip { overflow: hidden; }
body.egg-seesaw {
  animation: eggSeesaw 1s cubic-bezier(.36, .07, .19, .97) both;
  transform-origin: 50% 60%;
}
@keyframes eggSeesaw {
  0% { transform: rotate(0deg); }
  40% { transform: rotate(14deg); }   /* left side up */
  80% { transform: rotate(-12deg); }  /* swings through; right side up */
  100% { transform: rotate(0deg); }   /* seamless reset */
}
/* disco + seesaw at once: both animations must run, or the egg never fires
   while the disco background animation holds the body's animation slot */
body.boost-disco.egg-seesaw {
  animation:
    discoBg 6s linear infinite,
    eggSeesaw 1s cubic-bezier(.36, .07, .19, .97) both;
  transform-origin: 50% 60%;
}
/* MODAL WHILE IT IS UP. This was pointer-events:none, which meant a full-screen
   takeover was decoration: the customer could click products, open the cart and reach
   the checkout button through a black screen they could not see past, and any click
   landed on whatever happened to be under the pixel.

   The reason it was click-through is real, though, and it is why the escape hatch below
   exists: an overlay left in the DOM with pointer events on would eat every click on
   the page forever. Every construction site self-removes, and Escape force-clears
   whatever is up regardless. */
.egg-overlay {
  position: fixed; inset: 0; z-index: 80;
  display: flex; align-items: center; justify-content: center; text-align: center;
  opacity: 0; transition: opacity .4s ease;
  /* Same guard, and it does NOT undo the modality. A takeover still blocks the shop for its
     whole visible life -- .show turns clicks back on and the element is removed when it
     finishes. What this closes is the window between creation and reveal, where an invisible
     full-page blocker would otherwise be one thrown error away. */
  pointer-events: none;
}
.egg-overlay.show { opacity: 1; pointer-events: auto; }
.egg-dim { background: rgba(22, 9, 14, .85); backdrop-filter: blur(3px); }
.egg-black { background: #000; }
.egg-clear { background: transparent; }
.egg-no {
  font-family: 'Poppins', sans-serif; font-weight: 700;
  font-size: clamp(9rem, 34vw, 26rem); line-height: 1; color: #ff3b5c;
  text-shadow: 0 0 60px rgba(255, 59, 92, .6);
}
.egg-overlay.show .egg-no { animation: eggPop .45s cubic-bezier(.2, 1.5, .4, 1) both; }
@keyframes eggPop { from { transform: scale(.35); } to { transform: scale(1); } }
.egg-404 {
  font-family: 'Courier New', ui-monospace, monospace; font-weight: 700;
  font-size: clamp(2.2rem, 8vw, 6rem); letter-spacing: .06em; line-height: 1.25;
  color: #39ff9e; text-shadow: 0 0 26px rgba(57, 255, 158, .7);
  animation: egg404blink .55s steps(2, start) infinite;
}
@keyframes egg404blink { 50% { opacity: .18; } }
.egg-laugh {
  position: absolute; font-size: var(--s, 2rem);
  transform: rotate(var(--r, 0deg)) scale(0);
  animation: eggLaugh 3s ease var(--d, 0s) both;
}
@keyframes eggLaugh {
  0% { transform: rotate(var(--r)) scale(0); opacity: 0; }
  12% { transform: rotate(var(--r)) scale(1.3); opacity: 1; }
  20% { transform: rotate(var(--r)) scale(1); }
  78% { transform: rotate(var(--r)) scale(1); opacity: 1; }
  100% { transform: rotate(var(--r)) scale(.5); opacity: 0; }
}
#search.egg-agent { color: #1c8a4f; font-weight: 700; letter-spacing: .03em; }
.egg-freeship {
  font-family: 'Poppins', sans-serif; font-weight: 700;
  font-size: clamp(3rem, 11vw, 9rem); line-height: 1.08; letter-spacing: .04em;
  color: #7dffce; text-shadow: 0 0 46px rgba(57, 255, 158, .75);
}
.egg-overlay.show .egg-freeship { animation: eggPop .4s cubic-bezier(.2, 1.5, .4, 1) both; }

/* agent 69: the logo goes full hacker for the mission briefing */
/* NO TRANSITION ON FILTER. Interpolating between two filter chains interpolates the
   hue-rotate inside them, so applying a tint swept the logo through every hue in between --
   a visible flash of the wrong colours before it settled. Green (82deg) and purple (225deg)
   have the furthest to travel from bare, which is why those two flared and orange (-14deg)
   did not. A tint is a state, not a journey; it snaps. Opacity still fades, which is what the
   ragdoll's return and the hacker egg actually use. */
.brand img { transition: opacity .4s ease; }
.brand img.logo-fadeout { opacity: 0; }
/* Specificity note: the persona tint rules (.brand img.logo-purple etc.) appear
   LATER in this file and set `filter` too, so at equal specificity they used to
   win and the CRT treatment came out as a tinted logo in a green box. The effect
   now strips tints before it starts (see forceBlackLogo in common.js), and this
   extra .site-header makes the cascade agree with that intent either way. */
.site-header .brand img.logo-hacker {
  /* the art is a dark glyph on a white card: grayscale kills any color cast,
     invert turns the card into the black tube, then the sepia chain paints
     the glyph phosphor green — no blue wash */
  filter: grayscale(1) invert(1) sepia(1) saturate(5) hue-rotate(78deg) brightness(1.12) contrast(1.25);
  background: #020a05; /* dead-tube glass behind the glyph */
  border: 3px solid #18251d; /* monitor bezel */
  border-radius: 9px;
  padding: 2px 6px;
  box-shadow: inset 0 0 14px rgba(0, 0, 0, .9), 0 0 20px rgba(57, 255, 158, .3);
  animation: crtGlow 2.8s ease-in-out infinite; /* also beats disco's hue loop */
}
/* scanlines + flicker laid over the tube (the img can't host a pseudo) */
.site-header .brand:has(> img.logo-hacker) { position: relative; }
.site-header .brand:has(> img.logo-hacker)::after {
  content: ''; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  border-radius: 9px;
  background:
    repeating-linear-gradient(0deg, rgba(0, 0, 0, .26) 0 1px, transparent 1px 3px),
    radial-gradient(ellipse at center, transparent 62%, rgba(0, 0, 0, .35) 100%);
  animation: crtFlicker 2.1s steps(1) infinite;
}
@keyframes crtGlow {
  0%, 100% { box-shadow: inset 0 0 14px rgba(0, 0, 0, .9), 0 0 18px rgba(57, 255, 158, .28); }
  50% { box-shadow: inset 0 0 14px rgba(0, 0, 0, .9), 0 0 26px rgba(57, 255, 158, .42); }
}
@keyframes crtFlicker {
  0%, 100% { opacity: .95; }
  7% { opacity: .78; }
  8% { opacity: .97; }
  23% { opacity: .9; }
  24% { opacity: 1; }
  56% { opacity: .84; }
  57% { opacity: .96; }
  81% { opacity: .9; }
}
/* slot-machine personas: tint the black logo without new art assets
   (sepia gives the art a warm base, hue-rotate steers it — linework survives) */
.brand img.logo-purple,
.logo-reel-col img.logo-purple {
  filter: sepia(1) saturate(3.2) hue-rotate(225deg) brightness(.96);
}
.brand img.logo-green,
.logo-reel-col img.logo-green {
  filter: sepia(1) saturate(3.4) hue-rotate(82deg) brightness(.9);
}
/* Green reads as black on a moving reel. It carried the darkest brightness (.9) and nearly the
   lowest saturation (3.4) of the four tints, against disco at 5 / 1.05 -- fine on a logo you
   are looking at, invisible on one going past at speed. Lifted for the REEL copies only, so
   the persona itself is exactly the colour it always was.

   AFTER the rule it overrides, not before. Both selectors carry identical specificity, so the
   cascade is decided purely by source order -- written above, this loses silently and the
   green stays as dark as it was. */
.logo-reel-col img.logo-green {
  filter: sepia(1) saturate(4.8) hue-rotate(82deg) brightness(1.04);
}
/* ROW 0 IS THE EXCEPTION. It is appended first in the loop that builds the column
   (app.js), which makes it :first-child -- and per reelRoll's own keyframe, it is also the
   row that ends the animation sitting motionless at translateY(0), not streaming past. The
   lift above exists only so a logo going past AT SPEED still reads; applied to the one row
   that is not moving, it instead sat there bright for the settle's last couple hundred
   milliseconds and then visibly dropped to the darker, correctly-tinted persona underneath
   the instant the reel began its opacity fade -- a hard, noticeable snap rather than a
   handoff between two layers that already matched. Restoring the normal tint here, ahead of
   time, means the fade is a swap between two identical-looking layers: nothing to see.
   Higher specificity than the rule above (an extra pseudo-class), so this wins regardless of
   source order -- written after it anyway, to read as the deliberate exception it is. */
.logo-reel-col img.logo-green:first-child {
  filter: sepia(1) saturate(3.4) hue-rotate(82deg) brightness(.9);
}
/* orange: the roast comic. Warmer and brighter than the others so the takeover
   reads instantly — tuned toward the #d97a2b / #f2a65a of the gear tile. */
.brand img.logo-orange,
.logo-reel-col img.logo-orange {
  filter: sepia(1) saturate(5.5) hue-rotate(-14deg) brightness(1.04);
}
/* reel copies of the disco face: the same vivid tint, but static -- it is only
   on screen for a few hundred milliseconds and an animation there is wasted
   compositor work. */
.logo-reel-col img.logo-disco {
  filter: sepia(1) saturate(5) hue-rotate(190deg) brightness(1.05);
}
/* disco: the logo joins the party with a slow rainbow shimmer */
@property --dh { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
.brand img.logo-disco {
  filter: sepia(1) saturate(4.5) hue-rotate(var(--dh)) brightness(1);
  animation: discoHue 6s linear infinite;
}
@keyframes discoHue { to { --dh: 360deg; } }
/* Disco's stand-in for the persona reel (see the boost-disco branch of slotLogo() in app.js):
   a plain 720° spin, left or right -- JS sets --disco-spin-dir to 1 or -1 fresh each time --
   with no persona swap underneath it. 720 rather than 360 so it reads as a deliberate flourish
   rather than a single lazy turn; either way it is a whole number of rotations, so the logo
   always lands back at the exact orientation it started in and removing the class afterwards
   is invisible. Only touches transform, so it runs alongside discoHue's filter animation
   above without either one fighting the other. */
.logo-disco-spin {
  animation: logoDiscoSpin .9s cubic-bezier(.3, .1, .2, 1) both;
}
@keyframes logoDiscoSpin {
  to { transform: rotate(calc(var(--disco-spin-dir, 1) * 720deg)); }
}
@media (prefers-reduced-motion: reduce) {
  .logo-disco-spin { animation: none; }
}
.logo-bubble.lb-hacker {
  background: #041207; border-color: #39ff9e; color: #39ff9e;
  font-family: 'Courier New', ui-monospace, monospace; font-weight: 700;
  text-shadow: 0 0 8px rgba(57, 255, 158, .8);
  box-shadow: 0 6px 24px rgba(57, 255, 158, .25);
}
.logo-bubble.lb-hacker::before { background: #041207; border-color: #39ff9e; }
/* the credits egg: a proper end-of-movie roll */
.credits-overlay {
  position: fixed; inset: 0; z-index: 92; overflow: hidden;
  background: rgba(6, 3, 5, .96);
  opacity: 0; transition: opacity .9s ease;
  cursor: pointer;
  /* Inert until revealed -- see the note on .screensaver. It is created at opacity 0 and given
     .show a frame later; anything that throws in between would leave an invisible sheet over
     the whole page that still takes every click. */
  pointer-events: none;
}
.credits-overlay.show { opacity: 1; pointer-events: auto; }
.credits-roll {
  position: absolute; left: 50%; top: 0; transform: translate(-50%, 100vh);
  width: min(720px, 88vw); text-align: center; color: var(--cream, #f7f2ee);
  font-family: 'Poppins', sans-serif;
  animation: creditsRoll 80s linear forwards;
}
/* the pause-capable version is steered by JS frame-by-frame */
.credits-roll.cr-jsdriven { animation: none; will-change: transform; }
@keyframes creditsRoll {
  from { transform: translate(-50%, 100vh); }
  to { transform: translate(-50%, -100%); }
}
.cr-h1 {
  font-size: clamp(1.6rem, 5vw, 2.6rem); font-weight: 700; letter-spacing: .18em;
  text-transform: uppercase; color: #d88e8e; margin: 0 0 1.6rem;
}
.cr-h2 {
  font-size: clamp(1.05rem, 3vw, 1.4rem); font-weight: 600; letter-spacing: .22em;
  text-transform: uppercase; color: #b33352; margin: 3.2rem 0 1.1rem;
}
.cr-name { font-size: clamp(.95rem, 2.6vw, 1.15rem); margin: .55rem 0; opacity: .92; }
/* Stats: label ...... value, table-of-contents style. A flex row rather than fixed dot
   characters -- the dotted rule is a border on an empty flex-grow spacer, so it fills
   exactly the gap left between whatever the label and value happen to measure, on any
   width, instead of a fixed run of dots falling short or wrapping. */
.cr-stat {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .3em .5em;
  font-size: clamp(.95rem, 2.6vw, 1.15rem); margin: .55rem 0; opacity: .92;
  max-width: min(560px, 88vw); margin-left: auto; margin-right: auto;
  text-align: left;
}
/* min-width: 0 overrides the automatic min-content floor a flex item gets by default --
   without it, a long label (there is a genuinely long one in here) cannot shrink below its
   own single-line width and pushes the row, and the page, sideways on a narrow phone. With
   it and ordinary (wrapping) white-space, long text wraps inside its own item instead. */
.cr-stat-label { flex: 1 1 auto; min-width: 0; }
.cr-stat-value { flex: 0 1 auto; min-width: 0; }
.cr-stat-leader {
  flex: 1 1 2em; align-self: center; min-width: 1.5em;
  border-bottom: 2px dotted rgba(247, 242, 238, .4);
}
/* The longest stat (label + value together measure past the row's own 560px cap at the
   ordinary size) gets a condensed size instead of being left to wrap onto a second line --
   scaled down by the same ratio the row is over budget by, checked against the real
   rendered width rather than guessed. */
.cr-stat.cr-stat-tight { font-size: clamp(.78rem, 2.13vw, .94rem); }
/* The Helene tribute. A step up from a name and fully opaque, so it reads as the line the
   section builds to rather than one more entry — but still short of a heading, which would
   make it an announcement instead of a remembrance. Not bold -- a single sentence carries
   its own weight without it. Margin matches .cr-h2's ("A Word From Nick") so it gets the
   same amount of air above and below it, rather than a name's tighter rhythm. */
.cr-tribute {
  font-size: clamp(1.05rem, 2.9vw, 1.3rem); font-weight: 400;
  margin: 3.2rem 0 1.1rem; opacity: 1; letter-spacing: .01em;
}
/* The vow, immediately under the dedication. Pulled up so the two read as one thought
   finishing rather than two separate statements, and tracked wider because it is four words
   carrying the weight of the sentence above it. Only margin-top is touched -- the bottom
   margin still comes from .cr-tribute above, matching the h2's own spacing beneath it. */
.cr-tribute.cr-vow { margin-top: -.15rem; letter-spacing: .1em; }
.cr-essay {
  font-size: clamp(.85rem, 2.2vw, 1rem); line-height: 1.75; text-align: left;
  margin: 2.6rem 0; opacity: .85; font-family: 'Roboto', sans-serif;
}
/* the essay reads as proper paragraphs now, grouped so the 60s pause centers
   the whole passage as one piece */
.cr-essayblock { margin: 2.6rem 0; }
.cr-essayblock .cr-essay { margin: 0 0 1.4rem; }
.cr-essayblock .cr-essay:last-child { margin-bottom: 0; }
.cr-quote {
  font-size: clamp(1.1rem, 3vw, 1.5rem); font-style: italic; color: #d88e8e;
  margin: 2.6rem 0 3rem;
}
.logo-bubble.lb-cinco {
  background: #10060a; border-color: #ff4a4a; color: #ff6b6b;
  font-family: 'Courier New', ui-monospace, monospace; font-weight: 700;
  text-shadow: 0 0 8px rgba(255, 74, 74, .8);
  box-shadow: 0 6px 24px rgba(255, 74, 74, .25);
}
.logo-bubble.lb-cinco::before { background: #10060a; border-color: #ff4a4a; }
.egg-nice { color: #7dffce; text-shadow: 0 0 60px rgba(57, 255, 158, .7); }
/* 420: BLAZE IT — huge letters actually on fire */
.egg-blazeit {
  font-family: 'Poppins', sans-serif; font-weight: 900;
  font-size: clamp(3rem, 13vw, 10rem); letter-spacing: .06em; line-height: 1.05;
  background: linear-gradient(0deg, #7a1602 0%, #e2540e 34%, #ff9b1c 62%, #ffe08a 88%, #fff7d6 100%);
  background-size: 100% 240%;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  animation: blazeLick 1.1s ease-in-out infinite alternate;
  filter: drop-shadow(0 0 18px rgba(255, 122, 24, .85)) drop-shadow(0 -8px 34px rgba(255, 176, 32, .55));
}
.egg-overlay.show .egg-blazeit { animation: blazeLick 1.1s ease-in-out infinite alternate, eggPop .45s cubic-bezier(.2, 1.5, .4, 1) both; }
@keyframes blazeLick {
  from { background-position: 0 100%; filter: drop-shadow(0 0 14px rgba(255, 110, 20, .8)) drop-shadow(0 -6px 26px rgba(255, 170, 30, .5)); }
  to { background-position: 0 55%; filter: drop-shadow(0 0 26px rgba(255, 140, 28, .95)) drop-shadow(0 -14px 44px rgba(255, 190, 40, .7)); }
}
/* 710 → OIL: the same glyphs, just upside down. rotateX+rotateY together land
   exactly on an in-plane 180° turn, so the tumble reads as OIL, then keeps
   tumbling back to 710 — forever, until the click. */
.egg-710-wrap { perspective: 900px; cursor: pointer; pointer-events: auto; /* the only clickable egg: the click is the exit */ }
.egg-710 {
  display: inline-block;
  font-family: 'Courier New', ui-monospace, monospace; font-weight: 700;
  font-size: clamp(4rem, 20vw, 15rem); line-height: 1; color: #ffd166;
  text-shadow: 0 0 40px rgba(255, 190, 80, .65), 0 0 90px rgba(255, 150, 40, .35);
  animation: flip710 7.5s cubic-bezier(.45, 0, .55, 1) infinite;
  transform-style: preserve-3d;
}
@keyframes flip710 {
  0%, 24% { transform: rotateX(0deg) rotateY(0deg); }        /* 710 */
  40%, 74% { transform: rotateX(180deg) rotateY(180deg); }   /* OIL */
  90%, 100% { transform: rotateX(360deg) rotateY(360deg); }  /* 710 again */
}
/* smoke weed everyday: 130 layered puffs — no two alike, no sound at all */
.egg-overlay.egg-sw { overflow: hidden; }
.egg-smoke {
  position: absolute; bottom: -20vh; width: var(--s, 5rem); height: var(--s, 5rem);
  border-radius: 46% 54% 52% 48% / 55% 45% 58% 42%; /* lumpy, not a perfect ball */
  background:
    radial-gradient(circle at 38% 42%, rgba(232, 238, 232, .34), rgba(196, 206, 198, .16) 55%, transparent 74%),
    radial-gradient(circle at 64% 60%, rgba(214, 222, 214, .22), transparent 68%);
  filter: blur(7px);
  animation:
    swRise var(--rise, 4.6s) cubic-bezier(.3, .1, .6, 1) var(--d, 0s) both,
    swSway calc(var(--rise, 4.6s) * .5) ease-in-out var(--d, 0s) infinite alternate;
}
.egg-smoke.sm-back { filter: blur(13px); opacity: .7; }
.egg-smoke.sm-front { filter: blur(4px); }
@keyframes swRise {
  from { transform: translate(0, 0) rotate(var(--rt, 0deg)) scale(.55); opacity: 0; }
  12% { opacity: .9; }
  62% { opacity: .55; }
  to { transform: translate(var(--sw, 0vw), -128vh) rotate(calc(var(--rt, 0deg) + 100deg)) scale(2.6); opacity: 0; }
}
@keyframes swSway {
  from { margin-left: 0; }
  to { margin-left: calc(var(--sw, 4vw) * -.4); }
}
.egg-sw-text {
  position: relative; text-align: center;
  font-family: 'Poppins', sans-serif; font-weight: 900;
  font-size: clamp(2.4rem, 9vw, 7rem); line-height: 1.12; letter-spacing: .08em;
  color: #b6e3b6; text-shadow: 0 0 46px rgba(120, 200, 120, .8);
}
.egg-overlay.show .egg-sw-text { animation: eggPop .5s cubic-bezier(.2, 1.5, .4, 1) both; }

/* ---------- logo slot machine: descending reel ---------- */
.brand {
  position: relative; display: inline-block;
  background: none; border: none; padding: 0; cursor: pointer;
}
.logo-bubble {
  position: absolute; left: calc(100% + 10px); top: 50%;
  transform: translateY(-50%) scale(.6); transform-origin: left center;
  background: #fff; border: 1px solid var(--line); border-radius: 12px;
  padding: .45rem .75rem; white-space: nowrap; z-index: 6;
  font-family: 'Poppins', sans-serif; font-weight: 600; font-size: .78rem;
  color: var(--raspberry); box-shadow: 0 6px 18px rgba(43, 34, 38, .18);
  opacity: 0; transition: opacity .2s ease, transform .25s cubic-bezier(.2, 1.4, .4, 1);
  pointer-events: none;
}
.logo-bubble::before {
  content: ''; position: absolute; left: -5px; top: 50%; margin-top: -5px;
  width: 10px; height: 10px; background: #fff;
  border-left: 1px solid var(--line); border-bottom: 1px solid var(--line);
  transform: rotate(45deg);
}
.logo-bubble.show { opacity: 1; transform: translateY(-50%) scale(1); }
.logo-bubble {
  white-space: nowrap; overflow: hidden; max-width: 340px;
  width: max-content; line-height: 1.4;
}
.lb-text { display: inline-block; }
/* the muted circle steps aside while the logo is talking */
.now-playing.np-muted.np-dodge {
  left: calc(var(--np-left, 140px) + 370px);
  transition: left .4s ease;
}
/* checkout header: the docked pill fades out of the bubble's way instead */
.site-header.slim .now-playing.np-dodge {
  opacity: 0; pointer-events: none;
  transition: opacity .45s ease;
}
/* The third case: the ordinary desktop pill (centred, or already shifted left by
   dodgeHeaderNeighbors() in muzak.js) has no dodge of its own above, even though it and a
   logo bubble both live in the same left-hand stretch of the header. Added by logoQuip()
   only when it measures a REAL overlap between the two rects, not unconditionally every
   time the logo talks -- most of the time, especially on a wide monitor, there is none.
   Three classes deep on purpose: .now-playing.np-playing/.np-idle below also transitions
   opacity now (the slower fade back IN once the bubble is gone), and that rule comes later
   in the file -- at equal (two-class) specificity the later rule wins ties, which would
   have silently swapped this fade-out for that fade-in duration. The extra class outranks
   it outright so the two directions can never fight over which duration applies. */
.now-playing.np-playing.np-bubble-fade,
.now-playing.np-idle.np-bubble-fade {
  opacity: 0; pointer-events: none;
  transition: opacity .45s ease;
}
.logo-reel {
  position: absolute; overflow: hidden; pointer-events: none;
  opacity: 1; transition: opacity .45s ease;
}
.logo-reel.fade { opacity: 0; }
.logo-reel-col { display: flex; flex-direction: column; will-change: transform; }
.logo-reel-col img { width: 100%; height: var(--h); object-fit: contain; display: block; }
.logo-reel-col.roll {
  animation:
    reelRoll 2.6s cubic-bezier(.16, .84, .28, 1) both,
    reelBlur 2.6s linear both;
}
@keyframes reelRoll {
  0% { transform: translateY(calc(var(--rows) * var(--h) * -1)); } /* start above */
  86% { transform: translateY(0); }                                 /* symbols fall DOWN into place */
  92% { transform: translateY(calc(var(--h) * -.16)); }             /* bounce */
  100% { transform: translateY(0); }                                /* settle on the first row */
}
@keyframes reelBlur {
  0%, 100% { filter: blur(0); }
  12%, 68% { filter: blur(2.5px); }
  86% { filter: blur(.4px); }
}
@media (prefers-reduced-motion: reduce) {
  body.egg-seesaw, .egg-overlay.show .egg-no, .egg-404, .egg-laugh,
  .logo-reel-col.roll, .np-scroll, .now-playing, body.boost-disco, body.boost-disco .site-header,
  body.boost-disco .ticker, body.boost-disco footer { animation: none; }
  .now-playing, .now-playing.np-playing, .now-playing.np-muted,
  .now-playing.np-idle, .now-playing.np-shift, .now-playing.np-force-dock { transition: none; }
  .egg-laugh { transform: rotate(var(--r, 0deg)) scale(1); }
}

/* global-search: result outside the chosen experience */
/* The off-experience warning. A full sentence on the card, on every device -- it replaced a
   16px "!" whose only explanation lived in a title attribute, which touch devices never show.
   Styled as a real control because it is one: tapping it switches experience. */
.exp-warn {
  display: block; width: 100%; margin: 0 0 .4rem; padding: .4rem .5rem;
  border: 1px solid #e3c775; border-radius: 0; background: #fdf6e3;
  color: #6b4e08; font: inherit; font-size: .72rem; font-weight: 600;
  line-height: 1.3; text-align: left; cursor: pointer;
  /* The card is clickable; this must not read as part of it. */
  position: relative; z-index: 1;
}
.exp-warn:hover, .exp-warn:focus-visible { background: #f9ecca; border-color: #d4b354; }
@media (max-width: 640px) {
  /* Cards are half-width here, so the sentence needs the room and a slightly tighter set. */
  .exp-warn { font-size: .68rem; padding: .35rem .4rem; line-height: 1.25; }
}

/* ---------- item detail modal ---------- */
.item-modal { position: fixed; inset: 0; z-index: 70; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.im-backdrop {
  position: absolute; inset: 0; background: rgba(43, 34, 38, .6);
  backdrop-filter: blur(4px); opacity: 0; transition: opacity .28s ease;
}
/* the dialog is the product tile, grown up: same sharp corners, same square
   photo, opened via a FLIP zoom straight out of the clicked card */
.im-card {
  position: relative; width: min(480px, 92vw); max-height: 88vh;
  display: flex; flex-direction: column;
  background: var(--panel, #fff); border-radius: 0; overflow: hidden;
  box-shadow: 0 34px 90px rgba(43, 34, 38, .5);
  opacity: 0;
  transition: opacity .3s ease, transform .5s cubic-bezier(.2, 1.25, .3, 1);
  will-change: transform;
}
.item-modal.show .im-backdrop { opacity: 1; }
.item-modal.show .im-card { opacity: 1; transform: none; }
.im-media { position: relative; background: #efe7e2; }
.im-media img {
  /* clean square, zoomed to fill — no letterbox margins, ever */
  display: block; width: 100%; aspect-ratio: 1; object-fit: cover;
}
.im-media.empty { display: none; }
.im-thumbs {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; gap: .4rem; padding: .5rem .6rem; overflow-x: auto;
  background: linear-gradient(0deg, rgba(43, 34, 38, .72), rgba(43, 34, 38, 0));
}
.im-thumb {
  flex: 0 0 50px; width: 50px; height: 50px; padding: 0; cursor: pointer;
  border: 2px solid rgba(247, 242, 238, .55); border-radius: 0; overflow: hidden; background: #fff;
  transition: border-color .15s ease, transform .15s ease;
}
.im-thumb:hover { transform: translateY(-2px); }
.im-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.im-thumb.active { border-color: var(--raspberry); }
.im-close {
  position: absolute; top: .65rem; right: .65rem; z-index: 1;
  width: 36px; height: 36px; border: none; border-radius: 50%; cursor: pointer;
  background: rgba(43, 34, 38, .55); color: #fff; font-size: .95rem; line-height: 1;
  transition: background .15s ease, transform .15s ease;
}
.im-close:hover { background: var(--raspberry); transform: rotate(90deg); }
/* ---------- description scroller + its visible rail ----------
   Long descriptions overflow, and a phone browser hides its scrollbar until you
   are ALREADY scrolling — which is backwards: the customer has to discover the
   extra text by accident. So the rail is drawn here, shown only when there is
   genuinely more to read, and it reports position as well as existence. */
/* the right-hand half: scrolling text on top, buy controls pinned underneath */
.im-panel { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.im-scroller { position: relative; flex: 1 1 auto; min-height: 0; display: flex; }
.im-actions {
  flex: 0 0 auto; padding: .85rem 1.5rem 1.15rem; border-top: 1px solid var(--line);
  display: flex; flex-direction: column; gap: .55rem; background: var(--panel, #fff);
}
.im-actions .variation-select { width: 100%; }
.im-actions .variation-select.hidden { display: none; }
.im-stock { margin: 0; min-height: 1.1em; font-size: .82rem; color: var(--raspberry); }
.im-buyrow { display: flex; align-items: center; gap: .7rem; }
.im-qty { display: flex; align-items: center; gap: .1rem; border: 1px solid var(--line); border-radius: 0; }
.im-step {
  width: 38px; height: 38px; border: none; background: none; cursor: pointer;
  font-size: 1.1rem; line-height: 1; color: var(--ink);
}
.im-step:disabled { opacity: .35; cursor: default; }
.im-qty b { min-width: 26px; text-align: center; font-variant-numeric: tabular-nums; }
.im-add { flex: 1 1 auto; }
.im-add:disabled { opacity: .55; cursor: default; }
/* min-height:0 on both is what lets a flex child actually scroll instead of
   growing the card past its max-height. */
.im-body { flex: 1 1 auto; min-height: 0; padding: 1.15rem 1.5rem 1.5rem; overflow-y: auto; }
/* the native bar is replaced, not doubled up */
.im-body { scrollbar-width: none; }
.im-body::-webkit-scrollbar { width: 0; height: 0; }
.im-scroller.scrollable .im-body { padding-right: 1.9rem; } /* clear of the rail */
.im-scrollbar {
  position: absolute; top: 1.15rem; right: .55rem; bottom: 1.15rem; width: 6px;
  border-radius: 999px; background: rgba(43, 34, 38, .12);
  opacity: 0; pointer-events: none; transition: opacity .3s ease;
  /* above the bottom fade: ::after is generated as the last child and would
     otherwise paint over the rail, washing out exactly the end of the travel
     the customer is trying to read. */
  z-index: 2;
}
.im-scroller.scrollable .im-scrollbar { opacity: 1; }
.im-scrollthumb {
  position: absolute; left: 0; width: 6px; top: 0; height: 24px;
  border-radius: 999px; background: var(--raspberry);
  transition: transform .1s linear, height .1s linear;
}
/* A soft fade at the bottom edge: the rail says "there is more", the fade shows
   the text being cut off mid-sentence, which is the more instinctive signal.
   It lifts once you reach the end so it never sits there over the last line. */
.im-scroller::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2.4rem;
  background: linear-gradient(0deg, var(--panel, #fff), rgba(255, 255, 255, 0));
  opacity: 0; pointer-events: none; transition: opacity .3s ease; z-index: 1;
}
.im-scroller.scrollable:not(.at-end)::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .im-scrollthumb, .im-scrollbar, .im-scroller::after { transition: none; }
}
.im-body h3 { margin: 0 0 .25rem; font-family: 'Poppins', sans-serif; font-size: 1.25rem; color: var(--ink); }
.im-price { margin: 0 0 .8rem; color: var(--raspberry); font-weight: 700; font-size: 1.05rem; }
.im-desc { margin: 0; color: #5d5157; line-height: 1.6; font-size: .95rem; white-space: pre-wrap; }
.im-desc.empty { color: #a49aa0; font-style: italic; }
.imgwrap.clickable { cursor: zoom-in; }
.imgwrap.clickable img { transition: transform .25s ease; }
.imgwrap.clickable:hover img { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
  .im-card, .im-backdrop, .imgwrap.clickable img { transition: none; }
}

/* ---------- desktop item dialog: two equal squares ----------
   One rectangle made of two squares. Left square is the photo at full size with
   its thumbnail strip; right square is the text plus the buy controls.

   The width is capped against vh as well as vw because the HEIGHT here is half
   the width — on a wide, short laptop a 920px card would be a 460px-tall
   rectangle that a 500px-tall viewport cannot show. 170vh keeps the resulting
   height under 85vh at every aspect ratio. */
@media (min-width: 900px) {
  /* Roughly 1.8x the previous area: 920px -> 1240px is 1.35x on each edge, and
     1.35 squared is 1.82. The vh term drops from 170 to 160 because the card is
     now genuinely tall enough for it to matter -- height is half the width, so
     160vh wide caps the height at 80vh and leaves room for the dialog's own
     padding on a 768px laptop. */
  .im-card { flex-direction: row; width: min(1664px, 96vw, 160vh); }
  .im-media { flex: 0 0 50%; width: 50%; aspect-ratio: 1; }
  /* fill the square exactly: the width-driven aspect-ratio on the img would
     otherwise win and leave a sliver of panel visible below it */
  .im-media img { height: 100%; aspect-ratio: auto; }
  .im-panel { flex: 0 0 50%; width: 50%; aspect-ratio: 1; }
  /* At this size the old type looked stranded: a 620px square with 15px body
     copy reads as a mostly-empty page rather than a product page. */
  .im-body { padding: 1.9rem 2.2rem 1.6rem; }
  .im-body h3 { font-size: 1.85rem; }
  .im-price { font-size: 1.3rem; margin-bottom: 1.1rem; }
  .im-desc { font-size: 1.08rem; line-height: 1.66; }
  .im-actions { padding: 1rem 1.9rem 1.5rem; }
  .im-scroller.scrollable .im-body { padding-right: 2.3rem; }
  .im-scrollbar { top: 1.9rem; bottom: 1.6rem; right: .9rem; }
  /* an item with no photo should not open as a half-width sliver */
  .im-card:has(.im-media.empty) { width: min(900px, 94vw); }
  .im-card:has(.im-media.empty) .im-panel { flex: 1 1 auto; width: auto; aspect-ratio: auto; }
}

/* responsive header labels */
.call-short, .sw-short { display: none; }
/* On a phone the Find Us button is the phone emoji alone: the header at 640px is a
   single nowrap row (logo, search, cart) and "Find" was the label competing hardest for
   the space it did not have. The emoji is aria-hidden and the button carries an
   aria-label, so the accessible name survives the text disappearing — otherwise this
   trade buys width by leaving screen-reader users with an unnamed button. The caret
   stays: it is what says this opens a menu rather than dialling immediately. */
.call-icon { display: none; }
@media (max-width: 640px) {
  .call-full, .sw-full { display: none; }
  .call-short, .sw-short { display: inline; }
  /* Scoped to this button so the shared .call-short rule above is untouched elsewhere. */
  .findus > .call-btn .call-short { display: none; }
  .findus > .call-btn .call-icon { display: inline; font-size: 1.05rem; line-height: 1; }
  .site-header { flex-wrap: nowrap; gap: .45rem; padding: .45rem .7rem; }
  .brand img { height: 38px; }
  .header-actions { margin-left: auto; gap: .35rem; align-items: center; }
  .btn.exp-switch { padding: .5rem .65rem; font-size: 1rem; line-height: 1; }
  .btn.pill.call-btn { padding: .5rem .8rem; }
  .cart-btn { padding: .5rem .7rem; }
  .exp-hint { display: none; } /* hints crowd a phone header */

  /* THE PHOTO IS NOT ALLOWED TO BE THE BIGGER HALF.
     A square photo at 92vw was taking ~60% of a short phone's dialog, which left
     the description in a four-line slot under it — the text is the reason the
     dialog was opened. Landscape crop plus a hard vh cap: at the 88vh the card
     tops out at, the photo can never exceed 34vh and the words always get the
     larger share. object-fit stays `cover`, so the cap crops rather than
     letterboxing. */
  .im-media { flex: 0 0 auto; }
  .im-media img { aspect-ratio: 4 / 3; max-height: 34vh; }
  .im-body { padding: 1rem 1.15rem 1.25rem; }
  .im-actions { padding: .7rem 1.15rem 1rem; }
  .im-scroller.scrollable .im-body { padding-right: 1.6rem; }
  .site-header.quipping .header-actions,
  .site-header.quipping .tabs {
    opacity: 0; pointer-events: none; transition: opacity .25s ease;
  }
}
.header-actions { transition: opacity .25s ease; }

/* ---------- rail overflow arrows ---------- */
.rail-wrap { position: relative; }
.rail-arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 40px; height: 40px; border: none; border-radius: 50%; cursor: pointer;
  background: rgba(43, 34, 38, .72); color: #fff; font-size: 1rem; line-height: 1;
  box-shadow: 0 4px 14px rgba(43, 34, 38, .35);
  display: flex; align-items: center; justify-content: center;
  animation: railNudge 1.6s ease-in-out infinite;
}
.rail-arrow.left { left: 6px; }
.rail-arrow.right { right: 6px; }
.rail-arrow:hover { background: var(--raspberry); }
@keyframes railNudge { 50% { transform: translateY(-50%) scale(1.12); } }
/* tablets & phones: the rail scrolls instead of wrapping (arrows hint at
   the overflow — no fade overlay, just clean arrows) */
@media (max-width: 1024px) {
  .cat-tiles {
    display: flex; flex-wrap: nowrap; justify-content: flex-start;
    overflow-x: auto; gap: 0;
    scroll-snap-type: x mandatory; padding: .1rem .1rem .5rem;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  .cat-tiles::-webkit-scrollbar { display: none; }
  .cat-tile { flex: 0 0 122px; width: 122px; scroll-snap-align: start; }
}

/* ---------- browsing soundtrack (muzak) ---------- */
.muzak-row { margin-top: 1.6rem; text-align: center; }
.muzak-label {
  font-family: 'Poppins', sans-serif; font-weight: 600; font-size: .8rem;
  letter-spacing: .14em; color: var(--raspberry); margin: 0 0 .6rem;
}
.muzak-sub {
  display: block; font-weight: 300; font-size: .68rem; letter-spacing: .06em;
  text-transform: none; color: var(--muted, #8a7a80); margin-top: .2rem;
}
.muzak-chips { display: flex; flex-wrap: wrap; gap: .5rem; justify-content: center; }
.muzak-chip {
  font-family: 'Poppins', sans-serif; font-weight: 500; font-size: .78rem;
  padding: .5rem 1.1rem; border-radius: 999px; cursor: pointer;
  background: transparent; color: var(--raspberry);
  border: 1.5px solid rgba(179, 51, 82, .45);
  transition: all .15s ease;
}
.muzak-chip:hover { border-color: var(--raspberry); background: rgba(179, 51, 82, .08); }
.muzak-chip.active {
  background: var(--raspberry); color: #fff; border-color: var(--raspberry);
  box-shadow: 0 4px 16px rgba(179, 51, 82, .35);
}
/* ---------- now-playing widget (header) ---------- */
/* The now-playing pill borrows the screensaver player's LAYOUT — static
   letterspaced caption, caret, station picker — but keeps the storefront's own
   light palette. `overflow` is visible so the picker can hang below it. */
.now-playing {
  position: absolute; top: 50%; z-index: 5;
  height: 38px; border-radius: 999px;
  background: #fff; border: 1px solid var(--line);
  color: var(--ink); cursor: pointer;
  display: flex; align-items: center; gap: .7rem; padding: 0 .9rem;
  white-space: nowrap;
}
.now-playing .np-icon {
  display: flex; flex: 0 0 auto; background: none; border: none; padding: 0;
  color: inherit; cursor: pointer;
}
.now-playing .np-icon svg { width: 18px; height: 18px; display: block; }
/* the label is also the station picker */
.np-track {
  position: relative; flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: .35rem;
  cursor: pointer; overflow: hidden;
}
.np-track::after { /* a small caret, so it reads as openable */
  content: ''; flex: 0 0 auto;
  width: 0; height: 0; margin-left: .1rem;
  border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-top: 5px solid var(--muted);
  transition: transform .25s ease;
}
.now-playing.np-open .np-track::after { transform: rotate(180deg); }
.np-scroll {
  display: block; overflow: hidden; text-overflow: ellipsis;
  font-family: 'Poppins', sans-serif; font-weight: 600;
  font-size: .72rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--raspberry);
}
/* volume slider: starts at 50%, buyer can push it either way */
/* The whole section is the target: a 4px-tall native track is a cruel thing to
   ask anyone to hit, so a click anywhere in here snaps to that horizontal
   percentile (see the wrap handler in muzak.js). */
.np-vol-wrap {
  flex: 0 0 auto; display: flex; align-items: center;
  align-self: stretch;            /* full pill height = a real hit area */
  padding: 0 .15rem; cursor: pointer;
}
.np-vol {
  flex: 0 0 74px; width: 74px; margin: 0; cursor: pointer;
  accent-color: var(--raspberry); height: 4px;
}
/* Hide the whole SECTION, not just the input. The wrap is a flex item with its
   own padding, so leaving it in the row kept ~16px of empty space (its padding
   plus the pill's gap) beside the icon — which pushed the icon left of centre in
   the 38px/44px muted circle. */
.now-playing.np-muted .np-vol-wrap { display: none; }
/* PLAYING: a pill in the center of the header.
   Order on resume: slide back to center FIRST, then expand. */
.now-playing.np-playing,
.now-playing.np-idle {
  left: 50%; width: min(340px, 38vw); transform: translate(-50%, -50%);
  transition:
    left 1.1s ease-in-out,
    width .45s ease 1.1s,
    padding .45s ease 1.1s,
    opacity 1s ease;
}
/* SHIFTED: dodgeHeaderNeighbors() in muzak.js measures the free gap between .tabs and
   .header-actions on every resize/experience change, and sets --np-shift-left only when
   the centred position above would overlap the THC hint or the experience switch button --
   the one collision possible here, since the pill is the only position:absolute element in
   an otherwise ordinary flex header. Higher specificity than the base rule above wins
   without needing !important. */
.now-playing.np-shift.np-playing,
.now-playing.np-shift.np-idle {
  left: var(--np-shift-left, 50%); transform: translate(0, -50%);
  transition: left .3s ease, width .45s ease, padding .45s ease;
}
/* NO ROOM AT ALL: even the leftmost legal position would still overlap a neighbour, which
   only happens on a "desktop" width tight enough that the header simply has nowhere left to
   put a 200px+ pill. Rather than let it collide anyway, it drops just below the sticky
   header instead -- NOT to the bottom of the viewport the way the real ≤1024px dock does:
   docked at the bottom on a tall desktop window, the genre picker (which opens DOWNWARD --
   see .np-menu -- this is not the real mobile dock, so it does not inherit that breakpoint's
   upward-opening override) would try to open below the visible viewport entirely, landing
   off-screen and unclickable. Anchored under the header instead, the same downward-opening
   menu always has the rest of the page to open into. */
.now-playing.np-force-dock {
  position: fixed; top: calc(var(--header-h, 82px) + 14px); left: 50%; bottom: auto; z-index: 45;
  transform: translate(-50%, 0);
  width: min(560px, calc(100vw - 28px)); height: 44px;
  box-shadow: 0 6px 20px rgba(43, 34, 38, .25);
  transition: width .45s ease, padding .45s ease;
}
/* MUTED: shrink into a circle FIRST, then glide slowly & steadily to the logo. */
.now-playing.np-muted {
  left: var(--np-left, 140px); width: 38px; transform: translate(0, -50%);
  padding: 0; justify-content: center; color: #a49aa0;
  transition:
    width .45s ease,
    padding .45s ease,
    left 1.1s ease-in-out .45s;
}
.now-playing.np-muted .np-track { display: none; }
.now-playing:hover { border-color: var(--raspberry); }
/* SETTLING: the pill is growing back into place — from the muted circle in the
   header, or from the tucked circle on a phone. The volume slider and the station
   list stay out of it until it has stopped moving; both were rendering instantly
   and travelling along with the pill. Applies at every size, because the pill
   animates at every size. */
/* Fades IN when the flight ends, but hides INSTANTLY when it begins — a
   symmetrical transition meant the slider spent 300ms fading out, which looked
   exactly like it had appeared and then thought better of it. */
.now-playing .np-track,
.now-playing .np-vol-wrap { opacity: 1; transition: opacity .3s ease; }
/* While the pill is changing shape, the icon must be the ONLY thing in the flex
   row — otherwise it is centred against icon+label+slider and drifts left of the
   circle. Taking the others out with position:absolute rather than display:none
   keeps them rendered, so they can still fade back in when the flight ends. */
.now-playing.np-settling { justify-content: center; }
.now-playing.np-settling .np-track,
.now-playing.np-settling .np-vol-wrap {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  opacity: 0; pointer-events: none; transition: none;
}
.now-playing.np-settling .np-menu { display: none; }

/* IDLE: no station yet, so there is nothing to mute and no level to set — the
   pill is purely an invitation plus its station list. */
.now-playing.np-idle .np-icon,
.now-playing.np-idle .np-vol-wrap { display: none; }
.now-playing.np-idle .np-track { justify-content: center; }
.now-playing.np-idle .np-scroll {
  /* "Listen while you shop" is far longer than a genre name — tighter tracking
     and normal case so it reads as a sentence and still fits the pill */
  letter-spacing: .04em; text-transform: none; font-size: .74rem;
}
.now-playing.np-muted { color: #a49aa0; }

/* ---- the genre picker ---- */
.np-menu {
  position: absolute; top: calc(100% + .45rem); left: 50%;
  transform: translateX(-50%) translateY(-6px);
  min-width: 168px; max-height: 46vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: .1rem;
  padding: .35rem; border-radius: 14px;
  background: #fff; border: 1px solid var(--line);
  box-shadow: 0 14px 34px rgba(43, 34, 38, .18);
  opacity: 0; pointer-events: none;
  transition: opacity .28s ease, transform .28s ease;
  z-index: 6;
}
.now-playing.np-open .np-menu {
  opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0);
}
/* muted state collapses the pill to a circle — no room for a label to click */
.now-playing.np-muted .np-menu { display: none; }
.np-menu-item {
  appearance: none; background: transparent; border: none; cursor: pointer;
  text-align: left; padding: .45rem .6rem; border-radius: 9px;
  font-family: 'Poppins', sans-serif; font-weight: 600;
  font-size: .72rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink);
}
.np-menu-item:hover { background: rgba(179, 51, 82, .1); color: var(--raspberry); }
.np-menu-empty {
  padding: .45rem .6rem; font-size: .68rem; letter-spacing: .1em;
  color: var(--muted);
}

/* checkout header: the centre belongs to "Secure Checkout" — the pill docks
   beside the logo instead, width-capped so the two can never collide */
.site-header.slim .now-playing.np-playing,
.site-header.slim .now-playing.np-idle {
  left: var(--np-left, 140px); transform: translate(0, -50%);
  width: min(280px, calc(50vw - var(--np-left, 140px) - 105px));
  transition: left 1.1s ease-in-out, width .45s ease 1.1s, padding .45s ease 1.1s;
}

/* phones AND tablets: the header is too crowded — the widget becomes a
   floating dock. Playing = slim bottom bar; muted = bubble bottom-left. */
@media (max-width: 1024px) {
  .now-playing { position: fixed; top: auto; z-index: 45; box-shadow: 0 6px 20px rgba(43, 34, 38, .25); }
  .now-playing.np-playing,
  .now-playing.np-idle {
    left: 50%; bottom: 14px; width: min(560px, calc(100vw - 28px)); height: 44px;
    transform: translate(-50%, 0);
    transition: left .9s ease-in-out, bottom .9s ease-in-out, width .45s ease .9s, padding .45s ease .9s;
  }
  .now-playing.np-muted {
    left: 14px; bottom: 14px; width: 44px; height: 44px;
    transform: none;
    transition: width .45s ease, padding .45s ease, left .9s ease-in-out .45s;
  }
  .np-vol { flex: 0 0 92px; width: 92px; }

  /* Dark mode, but ONLY as a floating dock. Over the page (rather than inside
     the cream header) the pill needs to separate itself from whatever is
     scrolling underneath, so it borrows the screensaver player's glass. In the
     desktop header it stays light — see the base rule above. */
  .now-playing {
    background: rgba(43, 34, 38, .62);
    backdrop-filter: blur(10px);
    border-color: rgba(247, 242, 238, .22);
    color: var(--cream);
    box-shadow: 0 8px 24px rgba(13, 6, 11, .38);
  }
  .now-playing:hover { border-color: rgba(247, 242, 238, .5); }
  .now-playing.np-muted { color: rgba(247, 242, 238, .62); }
  .np-scroll { color: var(--cream); }
  .np-track::after { border-top-color: rgba(247, 242, 238, .75); }
  .np-vol { accent-color: var(--rose); }

  .np-menu {
    /* the dock sits at the bottom of the screen, so the picker opens UPWARD */
    top: auto; bottom: calc(100% + .45rem);
    transform: translateX(-50%) translateY(6px);
    background: rgba(30, 22, 26, .88);
    backdrop-filter: blur(12px);
    border-color: rgba(247, 242, 238, .22);
    box-shadow: 0 -14px 34px rgba(13, 6, 11, .45);
  }
  .np-menu-item { color: rgba(247, 242, 238, .88); }
  .np-menu-item:hover { background: rgba(247, 242, 238, .14); color: #fff; }
  .np-menu-empty { color: rgba(247, 242, 238, .6); }

  /* TUCKED: ignored for a few seconds, so it becomes a note and drifts most of
     the way off the left edge. Still tappable — that is how it comes back. The
     drift is `left`, not a transform, so it composes with the existing
     playing/muted transitions instead of fighting them. */
  .now-playing.np-tucked {
    left: -18px; bottom: 14px; width: 48px; height: 48px;
    padding: 0; gap: 0; transform: none;
    justify-content: flex-end; align-items: center;
    opacity: .8;
    transition: left .65s ease, width .35s ease, height .35s ease,
                opacity .35s ease, bottom .35s ease;
  }
  /* everything inside is hidden, including the picker — a tucked dock has one
     job and it is "come back" */
  .now-playing.np-tucked > * { display: none; }
  .now-playing.np-tucked::before {
    content: '\266a';
    display: block; margin-right: 11px;
    font-size: 1.15rem; line-height: 1; color: var(--cream);
  }
  .now-playing.np-tucked:hover { opacity: 1; }

  /* Tucked AND muted: same hiding place, different meaning. It shows the real
     mute icon rather than the note, so the circle says "your music is off" and
     not "pick something". Tapping it resumes playback — see the click handler in
     muzak.js. This is deliberately NOT the idle state: the two must never stack. */
  .now-playing.np-tucked.np-muted::before { content: none; }
  .now-playing.np-tucked.np-muted > .np-icon {
    display: flex; margin-right: 11px; color: var(--cream);
  }
  .now-playing.np-tucked.np-muted { opacity: .82; }
}

@media (prefers-reduced-motion: reduce) {
  .now-playing.np-tucked { transition: none; }
}

/* ---------- muzak reveal + panel (chooser page) ---------- */
.muzak-reveal {
  font-family: 'Poppins', sans-serif; font-weight: 500; font-size: .72rem;
  letter-spacing: .22em; text-transform: uppercase;
  padding: .55rem .2rem; cursor: pointer;
  background: transparent; color: #8a7a80;
  border: none; border-bottom: 1px solid rgba(138, 122, 128, .35);
  border-radius: 0; transition: color .18s ease, border-color .18s ease, letter-spacing .18s ease;
}
.muzak-reveal:hover { color: var(--raspberry); border-color: var(--raspberry); letter-spacing: .26em; }
.muzak-reveal.open { color: var(--raspberry); border-color: var(--raspberry); }
/* The death ban message (see DEATH_BAN_LABEL / paintMuzak in muzak.js) is a full sentence,
   not a short label -- the uppercase transform and wide tracking this button normally uses
   were sized for "WANT TO LISTEN TO MUSIC WHILE YOU SHOP?" and read poorly stretched across
   a whole accusation. :disabled only ever means this one state right now. */
.muzak-reveal:disabled {
  cursor: not-allowed; opacity: .8;
  text-transform: none; letter-spacing: normal; font-style: italic;
}
.muzak-panel { margin-top: 1rem; }
.muzak-note {
  font-family: 'Poppins', sans-serif; font-weight: 300; font-size: .8rem;
  color: #8a7a80; font-style: italic; margin: .2rem auto .8rem; max-width: 460px;
}
.muzak-note a { font-style: normal; font-weight: 600; }

.legal-music { font-size: .68rem; line-height: 1.55; color: var(--muted); text-align: justify; margin: .5rem 0; font-weight: 700; }

/* ---------- footer service block ---------- */
.footer-service {
  margin: 1.6rem auto; padding: 1.2rem 0; max-width: 720px;
  text-align: center; position: relative;
}
/* fancy line breaks: hairline gradients with a centered diamond */
.footer-service::before, .footer-service::after {
  content: '✦'; position: absolute; left: 0; right: 0;
  color: rgba(179, 51, 82, .55); font-size: .7rem; line-height: 0;
  background:
    linear-gradient(90deg, transparent, rgba(179, 51, 82, .35) 20%, rgba(179, 51, 82, .35) 42%, transparent 46%) left center / 100% 1px no-repeat,
    linear-gradient(90deg, transparent 54%, rgba(179, 51, 82, .35) 58%, rgba(179, 51, 82, .35) 80%, transparent) left center / 100% 1px no-repeat;
}
.footer-service::before { top: 0; }
.footer-service::after { bottom: 0; }
.ship-note { margin: 0 0 .8rem; color: #6b5e64; font-size: .82rem; line-height: 1.55; }
.service-links { display: flex; gap: .8rem; justify-content: center; flex-wrap: wrap; }
.svc-btn {
  font-family: 'Poppins', sans-serif; font-weight: 600; font-size: .82rem;
  padding: .55rem 1.3rem; border-radius: 999px;
  border: 1.5px solid var(--raspberry); color: var(--raspberry);
  transition: all .15s ease;
}
.svc-btn:hover { background: var(--raspberry); color: #fff; opacity: 1; }

/* ---------- variation selector on product cards ---------- */
.variation-select {
  width: 100%; padding: .45rem .5rem; margin: .1rem 0 .15rem;
  border: 1px solid var(--line); border-radius: 8px;
  background: #fdfbfa; color: var(--ink); font-size: .85rem;
  font-family: 'Roboto', sans-serif;
}

/* ---------- mini cart / checkout handoff ---------- */
#cart-subtotal { padding: .5rem 0 .1rem; font-size: .95rem; }
.small { font-size: .78rem; margin: .2rem 0 .6rem; }
#to-checkout { display: block; text-align: center; text-decoration: none; }

/* ---------- secure checkout page ---------- */
.site-header.slim { justify-content: space-between; }
.secure-note {
  display: flex; align-items: center; gap: .45rem;
  font-family: 'Poppins', sans-serif; font-weight: 600; color: var(--raspberry);
}
.back-link { font-size: .9rem; }
.checkout-main { max-width: 1040px; margin: 0 auto; padding: 1.5rem; }
.checkout-columns {
  display: grid; gap: 1.25rem; grid-template-columns: 1fr 1.1fr; align-items: start;
}
.checkout-card {
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 1.25rem 1.4rem;
}
.checkout-card h2 { color: var(--raspberry); margin: 0 0 .7rem; font-size: 1.2rem; }
/* ---------- multistep checkout ---------- */
.ck-progress {
  list-style: none; display: flex; gap: .4rem; padding: 0; margin: 0 0 1.2rem;
}
.ck-progress .ckp {
  flex: 1; display: flex; align-items: center; gap: .45rem;
  font-family: 'Poppins', sans-serif; font-size: .78rem; font-weight: 600;
  color: #a49aa0; padding-bottom: .5rem; border-bottom: 3px solid #e7dcd5;
  transition: color .3s ease, border-color .3s ease;
}
.ck-progress .ckp-dot {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%; font-size: .72rem;
  background: #e7dcd5; color: #7c6f75; transition: background .3s ease, color .3s ease;
}
.ck-progress .ckp.active { color: var(--ink); border-color: var(--raspberry); }
.ck-progress .ckp.active .ckp-dot { background: var(--raspberry); color: #fff; }
.ck-progress .ckp.done { color: var(--ink); border-color: #8fbf9a; }
.ck-progress .ckp.done .ckp-dot { background: #8fbf9a; color: #fff; font-size: 0; }
.ck-progress .ckp.done .ckp-dot::before { content: '✓'; font-size: .72rem; }
.ck-step { display: none; }
.ck-step.active { display: block; animation: ckStepIn .35s ease both; }
@keyframes ckStepIn {
  from { opacity: 0; transform: translateX(14px); }
  to { opacity: 1; transform: none; }
}
.ck-nav { display: flex; align-items: center; gap: .8rem; margin-top: .9rem; }
.ck-nav .ck-next { flex: 1; }
.ck-back { white-space: nowrap; }
#addr-status:empty { display: none; }
.addr-ok { color: #2e7d4f !important; }
.addr-warn { color: #a35a12 !important; }
@media (prefers-reduced-motion: reduce) { .ck-step.active { animation: none; } }
.checkout-card.center { text-align: center; }
.qrow { display: flex; justify-content: space-between; padding: .28rem 0; font-size: .92rem; }
.qrow.total { border-top: 1px solid var(--line); margin-top: .35rem; padding-top: .55rem; font-weight: 700; font-size: 1.05rem; }
@media (max-width: 860px) {
  .checkout-columns { grid-template-columns: 1fr; }
}

/* ---------- success state ---------- */
.checkout-card h2.success-title {
  color: #1c8a4f; font-size: 2.4rem; letter-spacing: .04em; margin: .2rem 0 .4rem;
}
.success-sub { color: #1c8a4f; font-weight: 600; font-size: 1.05rem; margin: 0 0 .6rem; }

/* confirmation details: intentional rows + discreet reference */
.confirm-info {
  display: flex; flex-direction: column; gap: .5rem;
  max-width: 430px; margin: 1rem auto .2rem; text-align: left;
}
.confirm-row {
  display: flex; align-items: baseline; gap: .65rem;
  padding: .6rem .85rem; background: #faf6f3;
  border: 1px solid var(--line); border-radius: 10px; font-size: .92rem;
}
.confirm-row .ci { flex: 0 0 auto; }
.confirm-row b { font-weight: 600; color: var(--ink); white-space: nowrap; }
.confirm-row .val { margin-left: auto; color: #6b5e64; text-align: right; overflow-wrap: anywhere; }
.confirm-ref {
  font-size: .68rem; color: #b0a6ab; letter-spacing: .04em;
  margin: .9rem 0 0; text-align: center;
}
#checkout-msg.info { color: var(--muted); font-weight: 500; }
#checkout-msg.ok { color: #1c8a4f; font-weight: 700; }

/* ---------- stock limits ---------- */
.line-warn {
  color: var(--raspberry-dark); background: #f9e3e6; border-radius: 6px;
  font-size: .78rem; font-weight: 600; padding: .35rem .55rem; margin: .15rem 0 .4rem;
}
.btn.pill.disabled { background: #cbb9b3; pointer-events: none; }
.cart-line .inc:disabled, .cart-line .dec:disabled { opacity: .35; cursor: not-allowed; }

/* ---------- gift card toggle / savings ---------- */
.linklike {
  background: none; border: none; padding: 0; margin: .9rem 0 .3rem;
  color: var(--raspberry); font-family: 'Poppins', sans-serif; font-weight: 600;
  font-size: .92rem; cursor: pointer; text-decoration: none; display: block;
}
.qrow.savings { color: #1c8a4f; font-weight: 700; }
/* The VIP balance is a running total, not a saving on this order, so it gets the rule
   above it that .total has rather than the green of .savings — it sits below the earned
   and redeemed lines and closes them off. */
.qrow.vip-total {
  border-top: 1px solid var(--line); margin-top: .3rem; padding-top: .5rem;
  font-weight: 700; color: var(--ink);
}
.qrow.updating span:last-child { opacity: .35; }
@keyframes savings-pop { 0% { transform: scale(1); } 40% { transform: scale(1.12); } 100% { transform: scale(1); } }
.savings-pop { animation: savings-pop .5s ease; display: inline-block; }

/* ---------- money burst ---------- */
.money-burst { position: fixed; inset: 0; pointer-events: none; z-index: 90; overflow: hidden; }
.money-burst span {
  position: fixed; animation: money-fly 1.4s ease-out forwards; opacity: 0;
}
@keyframes money-fly {
  0% { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  100% { transform: translate(var(--dx), calc(-1 * var(--rise))) rotate(var(--rot)); opacity: 0; }
}

/* ---------- engage discount button ---------- */
.btn.engage { white-space: nowrap; }
.btn.engage.lit {
  background: var(--raspberry); color: #fff;
  animation: engage-pulse 1.2s ease-in-out infinite;
}
@keyframes engage-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(179, 51, 82, .45); }
  50% { box-shadow: 0 0 0 8px rgba(179, 51, 82, 0); }
}
.btn.engage.engaged { background: #1c8a4f; color: #fff; }

/* ---------- locked VIP phone ---------- */
input.locked { background: #efe8e3 !important; color: #8a7d82; }

/* ---------- on-screen receipt (success view) ---------- */
.receipt {
  max-width: 440px; margin: 0 auto 1rem; text-align: left;
  background: var(--cream); border: 1px dashed var(--line); border-radius: 10px;
  padding: 1rem 1.2rem;
}
.receipt .r-item { display: flex; justify-content: space-between; gap: .8rem; padding: .3rem 0; font-size: .9rem; }
.receipt .r-item .nm { flex: 1; }
.receipt .r-item .muted-line { color: var(--muted); font-size: .78rem; }
.receipt hr { border: none; border-top: 1px dashed var(--line); margin: .5rem 0; }
.receipt .qrow.grand { border-top: 2px solid var(--ink); margin-top: .35rem; padding-top: .5rem; font-weight: 700; font-size: 1.05rem; }

.extras-row { gap: 1.4rem !important; }
.extras-row .linklike { margin: .9rem 0 .3rem; }

/* ---------- experience chooser ---------- */
.exp-choice {
  text-align: center; padding: 3.2rem 1.5rem 3.6rem;
  background:
    radial-gradient(ellipse at top, rgba(216,142,142,.18), transparent 60%),
    var(--cream);
}
.exp-choice .eyebrow { color: var(--muted); letter-spacing: .2em; font-size: .8rem; margin: 0 0 .6rem; }
.exp-choice h1 {
  font-size: clamp(2rem, 5.5vw, 3.4rem); color: var(--raspberry);
  letter-spacing: .05em; margin: 0 0 .4rem;
}
.exp-sub { color: var(--muted); font-size: 1.05rem; margin: 0 0 2rem; font-style: italic; }
.exp-cards {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 420px));
  gap: 1.5rem; justify-content: center; margin-bottom: 1.6rem;
}
.exp-card {
  border: none; cursor: pointer; border-radius: 18px; padding: 2.6rem 2rem 2.2rem;
  color: #fff; text-align: center; display: flex; flex-direction: column; gap: .55rem;
  align-items: center; font-family: 'Poppins', sans-serif;
  transition: transform .18s ease, box-shadow .18s ease;
  box-shadow: 0 6px 20px rgba(43,34,38,.18);
}
.exp-card:hover { transform: translateY(-6px) scale(1.015); box-shadow: 0 16px 36px rgba(43,34,38,.28); }
.exp-elevated { background: linear-gradient(140deg, #2b1022 0%, #5a2440 50%, var(--raspberry) 100%); }
.exp-grounded { background: linear-gradient(140deg, #123f3a 0%, #2e7d6e 55%, #7fc4b2 100%); }
.exp-icon { display: block; line-height: 0; }
.exp-icon svg {
  width: 58px; height: 58px; color: #fff; opacity: .95;
  filter: drop-shadow(0 4px 12px rgba(0, 0, 0, .3));
}
.exp-name { font-size: 2rem; font-weight: 700; letter-spacing: .12em; }
.exp-tag {
  font-size: .75rem; font-weight: 700; letter-spacing: .22em;
  background: rgba(255,255,255,.18); border-radius: 999px; padding: .25rem .9rem;
}
.exp-desc { font-family: 'Roboto', sans-serif; font-size: .95rem; opacity: .92; max-width: 300px; }
.exp-cta {
  margin-top: .7rem; background: #fff; color: var(--ink); font-weight: 700;
  border-radius: 999px; padding: .55rem 1.4rem; font-size: .92rem; font-style: italic;
  display: inline-flex; flex-direction: column; align-items: center; /* arrow gets its own line */
}
.cta-arrow { display: block; font-style: normal; line-height: 1; margin-top: .15rem; }
.exp-accessories {
  /* the third experience: a brushed-silver bar under the two big cards --
     smaller than them, but clearly outranking the music button below it */
  display: block; width: min(880px, 92vw); margin: 1.1rem auto 0;
  padding: 1.15rem 1.6rem; cursor: pointer; position: relative; overflow: hidden;
  border: none; border-radius: 14px;
  background: linear-gradient(135deg, #8a3d10 0%, #d97a2b 55%, #f2a65a 100%);
  color: #fff; font-family: 'Poppins', sans-serif; font-weight: 700;
  font-size: 1.05rem; letter-spacing: .04em;
  box-shadow: 0 6px 18px rgba(138, 61, 16, .35);
  transition: transform .15s ease, box-shadow .15s ease;
}
/* light sweep, same as the category tiles */
.exp-accessories::after {
  content: ''; position: absolute; inset: -60%;
  background: linear-gradient(115deg, transparent 44%, rgba(255, 255, 255, .38) 50%, transparent 56%);
  animation: all-sheen 4.2s ease-in-out infinite;
  pointer-events: none;
}
.exp-accessories:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(138, 61, 16, .45);
}
.btn.exp-switch {
  background: #fff; border: 2px solid var(--raspberry); color: var(--raspberry);
  border-radius: 999px; font-family: 'Poppins', sans-serif; font-weight: 600;
}
.btn.exp-switch:hover { background: var(--raspberry); color: #fff; }

/* ---------- ambient bloom + sparkles (compositor-only animations) ---------- */
.ambient {
  position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden;
  transition: filter 1.6s ease; /* boost changes glide instead of snapping */
  /* cart-value boost: 0 (empty) → 1 ($600+). JS sets --boost; the palette
     just gets more vivid — hue stays in-family. */
  filter: saturate(calc(1 + var(--boost, 0) * 2.4)) brightness(calc(1 + var(--boost, 0) * .05));
}
/* escalating tiers (body class set by JS from cart subtotal).
   --sboost MULTIPLIES each spark's inline --s (inline vars would beat a
   straight --s override). */
body.boost-1 { --sboost: 1.12; }
body.boost-2 { --sboost: 1.28; }
body.boost-3 { --sboost: 1.5; }
body.boost-4 { --sboost: 1.75; }
body.boost-2 .spark { animation-duration: 3.4s; }
body.boost-3 .spark { animation-duration: 2.6s; }
body.boost-4 .spark { animation-duration: 1.9s; }
/* $1,000+ cart: FULL DISCO 🪩 — rainbow room, glitter inverted.
   Header, ticker and footer join in, phase-shifted for a wave effect. */
body.boost-disco { animation: discoBg 6s linear infinite; }
body.boost-disco .site-header { animation: discoBg 6s linear -1.5s infinite; }
body.boost-disco .ticker { animation: discoBg 6s linear -3s infinite; }
body.boost-disco footer { animation: discoBg 6s linear -4.5s infinite; }
@keyframes discoBg {
  /* the house palette on a mirror ball: plum, raspberry, rose, sand, terracotta */
  0% { background-color: #ecc8d7; }
  20% { background-color: #d9b3c9; }
  40% { background-color: #f2cfc9; }
  60% { background-color: #eddbc4; }
  80% { background-color: #e3b8a0; }
  100% { background-color: #ecc8d7; }
}
body.boost-disco .spark { filter: invert(1); }
body.boost-3 .ghost, body.boost-4 .ghost { animation-duration: 15s; }
body.boost-4 .bloom.b1 { animation-duration: 22s; }
body.boost-4 .bloom.b2 { animation-duration: 28s; }
body.boost-4 .bloom.b3 { animation-duration: 25s; }
/* top tiers: slow psychedelic hue drift over the whole ambient layer */
body.boost-3 .ambient, body.boost-4 .ambient { animation: ambientTrip 9s ease-in-out infinite; }
@keyframes ambientTrip {
  0%, 100% {
    filter: saturate(calc(1 + var(--boost, 0) * 2.4)) brightness(calc(1 + var(--boost, 0) * .05)) hue-rotate(0deg);
  }
  50% {
    filter: saturate(calc(1.4 + var(--boost, 0) * 2.4)) brightness(calc(1 + var(--boost, 0) * .07)) hue-rotate(30deg);
  }
}
.ticker, .hero, .exp-choice, .cat-section, main, footer,
.checkout-main { position: relative; z-index: 1; }
/* the header keeps its own position: STICKY (set in the header section) --
   adding it to the rule above silently killed stickiness once */
/* NOTE: #cart-panel / #cart-backdrop are intentionally NOT in the rule above:
   they are position:fixed overlays (z-index 70/69 in their own blocks) and
   must never be demoted to in-flow elements. */
.bloom {
  position: absolute; border-radius: 50%; will-change: transform;
  /* alpha-falloff gradient = soft glow without any blur() filter */
}
.bloom.b1 {
  width: 55vw; height: 55vw; left: -18vw; top: -12vw;
  background: radial-gradient(circle, rgba(216,142,142,.30) 0%, rgba(216,142,142,0) 65%);
  animation: drift1 46s ease-in-out infinite alternate;
}
.bloom.b2 {
  width: 48vw; height: 48vw; right: -16vw; top: 22vh;
  background: radial-gradient(circle, rgba(174,211,212,.28) 0%, rgba(174,211,212,0) 65%);
  animation: drift2 58s ease-in-out infinite alternate;
}
.bloom.b3 {
  width: 42vw; height: 42vw; left: 28vw; bottom: -20vw;
  background: radial-gradient(circle, rgba(179,51,82,.16) 0%, rgba(179,51,82,0) 65%);
  animation: drift3 52s ease-in-out infinite alternate;
}
@keyframes drift1 { to { transform: translate(9vw, 7vh) scale(1.12); } }
@keyframes drift2 { to { transform: translate(-8vw, -6vh) scale(1.08); } }
@keyframes drift3 { to { transform: translate(-6vw, -9vh) scale(1.15); } }
.spark {
  position: absolute; left: var(--x); top: var(--y);
  width: 34px; height: 34px; border-radius: 50%;
  /* soft orb via gradient falloff -- still zero filter cost */
  background: radial-gradient(circle, rgba(255,255,255,.95) 0%, rgba(216,142,142,.4) 35%, rgba(216,142,142,0) 70%);
  opacity: 0; transform: scale(.35);
  animation: twinkle 4.2s ease-in-out var(--d) infinite;
  will-change: opacity, transform;
}
.spark.gold {
  background: radial-gradient(circle, rgba(255,240,200,.95) 0%, rgba(255,200,90,.35) 35%, rgba(255,200,90,0) 70%);
}
@keyframes twinkle {
  0%, 100% { opacity: 0; transform: scale(.35) translateY(0); }
  14% { opacity: calc(.55 * var(--s, 1) * var(--sboost, 1)); }
  26% { opacity: calc(.9 * var(--s, 1) * var(--sboost, 1)); transform: scale(calc(var(--s, 1) * var(--sboost, 1))) translateY(-6px); }
  46% { opacity: 0; transform: scale(.45) translateY(-14px); }
}

/* ---------- ghostly shimmer wisps ---------- */
.ghost {
  position: absolute; width: 34vw; height: 60vh; border-radius: 50%;
  background: radial-gradient(ellipse 45% 50% at 50% 50%,
    rgba(255,255,255,.13) 0%, rgba(216,142,142,.09) 40%, rgba(174,211,212,0) 72%);
  opacity: 0; will-change: transform, opacity;
  transform: skewX(-8deg);
}
.ghost.g1 { left: 4%; bottom: -30vh; animation: ghost-rise 26s ease-in-out 0s infinite; }
.ghost.g2 { left: 42%; bottom: -34vh; animation: ghost-rise 32s ease-in-out 9s infinite; }
.ghost.g3 { left: 72%; bottom: -28vh; animation: ghost-rise 29s ease-in-out 17s infinite; }
@keyframes ghost-rise {
  0% { transform: translateY(0) skewX(-8deg) scaleX(1); opacity: 0; }
  18% { opacity: .8; }
  38% { transform: translateY(-38vh) skewX(6deg) scaleX(1.18); opacity: .5; }
  58% { transform: translateY(-72vh) skewX(-5deg) scaleX(.9); opacity: .65; }
  80% { opacity: .25; }
  100% { transform: translateY(-115vh) skewX(4deg) scaleX(1.1); opacity: 0; }
}

/* Accessibility + battery: kill all decorative motion when the OS asks */
@media (prefers-reduced-motion: reduce) {
  .bloom, .spark, .ghost, .ambient, .cat-tile::after, .exp-accessories::after, .hero-slide.active .hero-img,
  .ticker-track, .hero-dot.active span, .btn.engage.lit { animation: none !important; }
  .spark { opacity: .35; transform: scale(.7); }
}

/* ---------- mobile: category tiles become a swipeable rail ---------- */
@media (max-width: 640px) {
  .cat-tiles {
    display: flex; flex-wrap: nowrap; justify-content: flex-start;
    overflow-x: auto; gap: 0;
    scroll-snap-type: x mandatory; padding: .1rem .1rem .5rem;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  .cat-tiles::-webkit-scrollbar { display: none; }
  .cat-tile { flex: 0 0 102px; width: 102px; scroll-snap-align: start; }
  .tile-icon svg { width: 28px; height: 28px; }
  .tile-label { font-size: .56rem; letter-spacing: .05em; padding: 0 .2rem; }
  .cat-tile.all-tile .all-label { font-size: .9rem; }
}

/* ---------- footer / legal ---------- */
footer { text-align: left; }
.footer-inner { max-width: 1100px; margin: 0 auto; }
.footer-inner > p:first-child { text-align: center; font-size: .85rem; }
.legal p { font-size: .68rem; line-height: 1.55; color: var(--muted); text-align: justify; margin: .5rem 0; }
.coa-line { text-align: center; font-size: .72rem; margin: .8rem 0 .3rem; }
.coa-line a { color: var(--raspberry); font-weight: 600; }
.copyright { text-align: center; font-size: .74rem; color: var(--muted); font-weight: 600; margin: .2rem 0 0; }

/* ---------- "Looking for THC?" header hint ---------- */
/* the hint IS the arrow now: text lives inside an arrow-shaped tag that
   sways toward the switch button — no separate glyph */
.exp-hint {
  position: relative;
  display: inline-flex; align-items: center;
  font-family: 'Poppins', sans-serif; font-style: italic; font-weight: 600;
  font-size: .8rem; color: #fff; white-space: nowrap;
  background: linear-gradient(135deg, #8a2440, var(--raspberry));
  padding: .38rem .55rem .38rem .8rem;
  border-radius: 8px 0 0 8px;
  box-shadow: 0 3px 10px rgba(179, 51, 82, .35);
  animation: hint-sway 2.4s ease-in-out infinite;
}
.exp-hint::after {
  /* the arrowhead, grown from the same box — overlaps 1px so no hairline
     seam can appear between rectangle and triangle at any zoom level */
  content: ''; position: absolute; left: calc(100% - 1px); top: 50%; transform: translateY(-50%);
  border-top: 1.05rem solid transparent; border-bottom: 1.05rem solid transparent;
  border-left: .8rem solid var(--raspberry);
}
.hint-lines { display: inline-flex; flex-direction: column; align-items: flex-end; line-height: 1.25; }
.tunes-hint { font-size: .64rem; font-weight: 500; color: rgba(255, 255, 255, .85); text-transform: uppercase; letter-spacing: .04em; }
@keyframes hint-sway {
  0%, 100% { transform: translateX(-5px); }
  50% { transform: translateX(5px); }
}

/* ---------- ELEVATED card watermark (triangle + leaf + !) ---------- */
.exp-card { position: relative; overflow: hidden; }
.exp-card > * { position: relative; z-index: 1; }
.exp-watermark {
  position: absolute !important; left: 50%; top: 50%;
  width: 72%; height: auto;
  transform: translate(-50%, -50%);
  color: #fff; opacity: .13; z-index: 0 !important;
  pointer-events: none;
  animation: watermark-breathe 9s ease-in-out infinite;
}
@keyframes watermark-breathe {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  50% { transform: translate(-50%, -50%) scale(1.05); }
}
@media (max-width: 640px) {
  .exp-hint { font-size: .72rem; }
}
@media (prefers-reduced-motion: reduce) {
  .exp-hint, .exp-watermark { animation: none !important; }
}

/* ---------- in-store marketing pages (welcome / mission / wholesale /
     returns / privacy). Same palette, type and ambient FX as the shop; prose
     column instead of a product grid. ---------- */
.info-page .page-nav {
  display: flex; flex-wrap: wrap; align-items: center; gap: .1rem .35rem;
  margin-left: auto; font-family: 'Poppins', sans-serif;
}
.info-page .page-nav a {
  padding: .3rem .55rem; border-radius: 999px;
  font-size: .78rem; font-weight: 600; letter-spacing: .02em;
  color: var(--ink); opacity: .72;
}
.info-page .page-nav a:hover { opacity: 1; background: rgba(179, 51, 82, .08); }
.info-page .page-nav a.on {
  opacity: 1; color: #fff; background: var(--raspberry);
}
.info-page .back-link { margin-left: .6rem; }
.info-main {
  width: min(760px, calc(100% - 3rem)); margin: 0 auto; padding: 2.4rem 0 3rem;
}
.info-hero { padding: 1rem 0 2.2rem; border-bottom: 1px solid var(--line); margin-bottom: 2rem; }
.info-hero h1 {
  font-size: clamp(2.1rem, 6vw, 3.4rem); line-height: 1.04; margin: .2rem 0 .5rem;
  letter-spacing: -.01em;
}
.info-kicker {
  font-family: 'Poppins', sans-serif; font-weight: 600;
  text-transform: uppercase; letter-spacing: .14em;
  font-size: .76rem; color: var(--raspberry); margin: .2rem 0;
}
.info-lede { font-size: 1.06rem; line-height: 1.62; }
.info-block { margin: 0 0 2.4rem; }
.info-block h2 {
  font-size: 1.32rem; margin: 0 0 .7rem; letter-spacing: -.005em;
}
.info-block h3 {
  font-size: 1.02rem; margin: 1.5rem 0 .45rem; color: var(--raspberry-dark);
}
.info-block p { line-height: 1.68; margin: 0 0 .95rem; }
.info-list { line-height: 1.66; padding-left: 1.15rem; margin: 0 0 1rem; }
.info-list li { margin-bottom: .5rem; }
.info-byline {
  font-family: 'Poppins', sans-serif; font-weight: 600; font-size: .86rem;
  color: var(--muted); letter-spacing: .03em;
}
.info-quote {
  margin: 0 0 1.4rem; padding: 1rem 1.2rem;
  background: rgba(255, 255, 255, .6); border-left: 3px solid var(--rose);
  border-radius: 0 12px 12px 0;
}
.info-quote p { margin: 0 0 .5rem; font-style: italic; }
.info-quote cite {
  font-family: 'Poppins', sans-serif; font-style: normal; font-weight: 600;
  font-size: .8rem; text-transform: uppercase; letter-spacing: .1em;
  color: var(--muted);
}
.info-cta { margin: 2.6rem 0 0; }
/* unfinished copy: loud on screen so a placeholder can never ship quietly */
.info-draft {
  border: 2px dashed var(--raspberry); border-radius: 14px;
  padding: 1.1rem 1.2rem; background: rgba(179, 51, 82, .05);
}
.info-flag {
  font-family: 'Poppins', sans-serif; font-weight: 700; font-size: .78rem;
  text-transform: uppercase; letter-spacing: .1em; color: var(--raspberry);
}
@media (max-width: 720px) {
  .info-page .site-header { flex-wrap: wrap; row-gap: .4rem; }
  .info-page .page-nav { width: 100%; margin-left: 0; justify-content: center; }
  .info-page .back-link { display: none; } /* the nav already has Shop */
  .info-main { width: calc(100% - 1.8rem); padding: 1.6rem 0 2.4rem; }
}

/* ---------- the visitor: a 1-in-1000 flyby (always on in demo mode) ----------
   Transform/opacity only, so it rides the compositor and costs almost nothing.
   Skipped entirely under prefers-reduced-motion (guarded in app.js too). */
.ufo-scene {
  position: fixed; inset: 0; z-index: 60;
  /* deliberately NOT pointer-events:none — while the visitor is on screen it
     swallows clicks, so nobody clicks past it. It removes itself when done. */
  pointer-events: auto;
  overflow: hidden; /* the trail runs wider than the viewport; no scrollbars */
  opacity: 1; transition: opacity 1.6s ease;
}
.ufo-scene.gone { opacity: 0; }
/* The page dims to 80% black behind the visitor, so the message is readable
   over whatever it happens to fly across. Faded in rather than snapped on, and
   it rides the scene's own fade on the way out. z-index 0 keeps it behind the
   craft (1) and the words (2). */
.ufo-scene::before {
  content: ''; position: absolute; inset: 0; z-index: 0;
  background: #050203; opacity: 0;
  transition: opacity .9s ease;
}
.ufo-scene.run::before { opacity: .8; }
/* the craft carries ONLY the horizontal crossing, so anything parented to it
   (the trail) tracks its position exactly */
.ufo-craft {
  position: absolute; top: 16vh; left: -14vw; width: 92px; height: 40px;
  z-index: 1; /* above the dim */
}
.ufo-scene.run .ufo-craft {
  animation: ufoCross 3.6s cubic-bezier(.42, 0, .58, 1) forwards;
}
/* the bob lives one level in, so the ship bobs and the trail stays level */
.ufo-rig { position: absolute; inset: 0; }
.ufo-scene.run .ufo-rig { animation: ufoBob 1.1s ease-in-out infinite; }
.ufo-body {
  position: absolute; left: 0; bottom: 8px; width: 92px; height: 22px;
  border-radius: 50%;
  background: linear-gradient(180deg, #cfd8e3 0%, #8a97a8 45%, #3c4654 100%);
  box-shadow: 0 0 18px rgba(160, 220, 255, .55);
}
.ufo-dome {
  position: absolute; left: 28px; bottom: 22px; width: 36px; height: 22px;
  border-radius: 50% 50% 0 0;
  background: radial-gradient(ellipse at 40% 80%, rgba(190, 255, 235, .95), rgba(90, 200, 190, .55));
  box-shadow: 0 0 14px rgba(150, 255, 230, .6);
}
.ufo-glow {
  position: absolute; left: 18px; bottom: -20px; width: 56px; height: 30px;
  border-radius: 50%;
  background: radial-gradient(ellipse at center top, rgba(180, 255, 240, .55), transparent 70%);
  filter: blur(3px);
}
/* The light trail, anchored to the middle of the craft and growing backwards.
   right:46px is half of the craft's 92px width, so the bright end of the streak
   sits under the ship for the whole crossing instead of racing ahead of it. */
.ufo-trail {
  position: absolute; right: 46px; top: 18px; height: 3px; width: 0;
  border-radius: 3px;
  background: linear-gradient(90deg, transparent, rgba(150, 255, 235, .9) 60%, rgba(255, 255, 255, .95));
  box-shadow: 0 0 22px rgba(150, 255, 235, .8);
  opacity: .95;
}
.ufo-scene.run .ufo-trail { animation: ufoTrail 3.6s cubic-bezier(.42, 0, .58, 1) forwards; }
.ufo-scene.speak .ufo-trail { opacity: 0; transition: opacity 1.1s ease; }
/* the trail resolves into the message, centred on the flight path */
.ufo-words {
  position: absolute; top: calc(16vh - 6px); left: 0; right: 0; z-index: 2;
  text-align: center;
  font-family: 'Poppins', sans-serif; font-weight: 600;
  font-size: clamp(1.1rem, 3.4vw, 2.1rem); letter-spacing: .06em;
  color: #eafff9; text-shadow: 0 0 26px rgba(150, 255, 235, .95), 0 0 60px rgba(90, 200, 190, .6);
  opacity: 0; transform: translateY(6px) scale(.98);
  transition: opacity 1.2s ease, transform 1.2s ease;
}
.ufo-scene.speak .ufo-words { opacity: 1; transform: translateY(0) scale(1); }
@keyframes ufoCross {
  from { left: -14vw; }
  to   { left: 108vw; }
}
@keyframes ufoBob {
  0%, 100% { transform: translateY(0) rotate(-2deg); }
  50%      { transform: translateY(-7px) rotate(2deg); }
}
@keyframes ufoTrail {
  from { width: 0; }
  to   { width: 122vw; } /* the full crossing distance: -14vw -> 108vw */
}
@media (prefers-reduced-motion: reduce) {
  .ufo-scene { display: none; }
}

/* screensaver: the rotating line under "LOVES YOU". Fades in and out on its own
   90-second cycle; opacity only, so it costs nothing while it sits there. */
/* One fixed spot: centred, directly below the title, gap set inline by
   common.js (SS_SUB_GAP) off the title's measured bottom edge -- no random band,
   no tilt. left/top and font-size are set inline; translate(-50%, 0) makes the
   left coordinate the horizontal centre and the top coordinate the box's own
   top edge, which is what that measurement assumes. */
.screensaver .ss-title { display: inline-block; }
/* The rotating quote. One or two lines, each its OWN nowrap element -- common.js
   decides the split, not the browser, so the breathing tracking below can never
   move a line break. Centred, so the two lines need not share a common width the
   way a justified pair would. */
.screensaver .ss-sub {
  position: absolute; z-index: 2;
  left: 50%; top: 62%;
  transform: translate(-50%, 0);
  display: block; text-align: center;
  font-family: 'Poppins', sans-serif; font-weight: 500;
  /* font-size, line-height and width are all set inline by the fitting pass */
  font-size: 20px; line-height: 29px;
  /* A little more air between the letters and the words. The fitting pass measures the text
     AFTER this applies, so the wrapping still comes out right. */
  letter-spacing: .045em; word-spacing: .12em;
  color: rgba(255, 245, 250, .92);
  text-shadow: 0 0 24px rgba(181, 126, 232, .55);
  opacity: 0; transition: opacity 1.2s ease;
}
/* --lsb is the shared breathe -- the "expanding" letter-spacing pulse. */
@property --lsb { syntax: '<length>'; initial-value: 0px; inherits: false; }
.screensaver .ss-line {
  display: block; white-space: nowrap; /* the guarantee: no line can ever wrap */
  hyphens: none;
  letter-spacing: .02em;       /* fallback */
  letter-spacing: var(--lsb);  /* the breathe */
  /* cancel the trailing gap after the last glyph -- centred text with an
     uncancelled gap on only one side would visibly drift right as it breathes */
  margin-right: calc(-1 * var(--lsb));
  animation: ssTrack 14s ease-in-out infinite; /* half speed: a slow breath */
}
/* Both lines share ONE random phase, set inline by renderSubLine, so they
   breathe together rather than at independent moments. */
@keyframes ssTrack {
  0%, 100% { --lsb: .02em; }
  50%      { --lsb: .14em; }
}
@media (prefers-reduced-motion: reduce) {
  .screensaver .ss-line { animation: none; }
}
/* 30% opacity: the quote sits well behind "LOVES YOU" in the visual hierarchy,
   not beside it. The 1.2s fade still runs, it just lands at .3 */
.screensaver .ss-sub.show { opacity: .3; }

/* ---------- the checkout dancers ----------
   Fifteen stick figures with pot-leaf heads cross the bottom of the screen.

   FIVE NESTED LAYERS, ONE JOB EACH. .sm-travel moves horizontally, .sm-flip
   tumbles about the waist, .sm-lean tilts, .sm-bob bounces, .sm-fig scales for
   depth. Collapsing any two means one transform-origin serving two transforms
   that want different ones -- the flip then swings the whole crossing off course.

   Every animation reads --sm-dur and --sm-delay off its own dancer, and every
   keyframe track is written in percentages, so one variable per dancer keeps the
   moonwalk, the tuck and the landing aligned at any tempo. `both` as the fill
   mode is load-bearing: without the backwards half, a dancer waiting out its
   delay would sit at translateX(0) in the middle of the screen. */
.sm-scene {
  position: fixed; left: 0; right: 0; bottom: 4vh; height: 250px;
  z-index: 65; pointer-events: none; overflow: hidden;
}
.sm-dancer { position: absolute; inset: 0; }
.sm-red { color: #c8402f; }
.sm-gold { color: #e0b430; }
.sm-green { color: #3f9142; }
/* ---------- one pass, or several ----------
   --sm-passes is how many times the troupe crosses, and `alternate` is what makes the
   extra ones a ROUND TRIP: every odd pass plays the same keyframes backwards, so the
   dancer runs back the way it came, and the backflip in the middle of the return leg
   tumbles the other way (a front flip) instead of repeating itself. Every track below
   shares the count, so the flip, the lean and all eight limb segments stay in phase
   across every pass without a second set of keyframes existing.

   It defaults to ONE, which is the confirmation screen's single crossing, unchanged.
   Party Mode asks for three -- see stickmanDance({ passes }) in common.js, which also
   holds the scene open long enough for them. At one iteration `alternate` does
   nothing at all, so the default costs nothing. */
.sm-travel {
  position: absolute; bottom: var(--sm-lift, 0); left: 0; width: 120px; height: 175px;
  animation: smTravel var(--sm-dur, 4s) linear var(--sm-delay, 0s) var(--sm-passes, 1) alternate both;
}
.sm-flip, .sm-lean, .sm-bob { position: absolute; inset: 0; }
/* the waist, so a backflip tumbles rather than pivoting on its heels */
.sm-flip { transform-origin: 50% 60%; animation: smFlip var(--sm-dur, 4s) ease-in-out var(--sm-delay, 0s) var(--sm-passes, 1) alternate both; }
.sm-lean { transform-origin: 50% 100%; animation: smLean var(--sm-dur, 4s) ease-in-out var(--sm-delay, 0s) var(--sm-passes, 1) alternate both; }
.sm-bob { animation: smBob calc(var(--sm-dur, 4s) / 7) ease-in-out var(--sm-delay, 0s) infinite both; }
.sm-fig {
  position: absolute; left: 50%; bottom: 0; width: 120px; margin-left: -60px; height: 175px;
  transform: scale(var(--sm-scale, 1)); transform-origin: 50% 100%;
}
.sm-head {
  position: absolute; left: 50%; top: 0; width: 56px; height: 34px; margin-left: -28px;
  animation: smNod calc(var(--sm-dur, 4s) / 9) ease-in-out var(--sm-delay, 0s) infinite both;
}
.sm-head svg { display: block; width: 100%; height: 100%; }
.sm-body {
  position: absolute; left: 50%; top: 33px; width: 3px; height: 63px; margin-left: -1.5px;
  background: var(--ink); border-radius: 2px;
}
.sm-arm, .sm-leg {
  position: absolute; left: 50%; width: 3px; margin-left: -1.5px;
  background: var(--ink); border-radius: 2px; transform-origin: top center;
}
/* Two-segment limbs. The lower segment hangs off the end of the upper one and
   swings from that joint, so its rotation composes with the shoulder's -- which
   is most of the difference between dancing and semaphore. */
.sm-fore, .sm-shin {
  position: absolute; left: 0; width: 3px;
  background: var(--ink); border-radius: 2px; transform-origin: top center;
}
.sm-arm { top: 41px; height: 20px; }
.sm-fore { top: 20px; height: 18px; }
.sm-leg { top: 95px; height: 24px; }
.sm-shin { top: 24px; height: 22px; }
.sm-arm.sm-l { animation: smArmL var(--sm-dur, 4s) ease-in-out var(--sm-delay, 0s) var(--sm-passes, 1) alternate both; }
.sm-arm.sm-r { animation: smArmR var(--sm-dur, 4s) ease-in-out var(--sm-delay, 0s) var(--sm-passes, 1) alternate both; }
.sm-arm.sm-l .sm-fore { animation: smForeL var(--sm-dur, 4s) ease-in-out var(--sm-delay, 0s) var(--sm-passes, 1) alternate both; }
.sm-arm.sm-r .sm-fore { animation: smForeR var(--sm-dur, 4s) ease-in-out var(--sm-delay, 0s) var(--sm-passes, 1) alternate both; }
.sm-leg.sm-l { animation: smLegL var(--sm-dur, 4s) ease-in-out var(--sm-delay, 0s) var(--sm-passes, 1) alternate both; }
.sm-leg.sm-r { animation: smLegR var(--sm-dur, 4s) ease-in-out var(--sm-delay, 0s) var(--sm-passes, 1) alternate both; }
.sm-leg.sm-l .sm-shin { animation: smShinL var(--sm-dur, 4s) ease-in-out var(--sm-delay, 0s) var(--sm-passes, 1) alternate both; }
.sm-leg.sm-r .sm-shin { animation: smShinR var(--sm-dur, 4s) ease-in-out var(--sm-delay, 0s) var(--sm-passes, 1) alternate both; }

@keyframes smTravel { from { transform: translateX(-16vw); } to { transform: translateX(112vw); } }
/* 0-38% moonwalk, 38-62% the flip, 62-100% out. The rotation ends on 360deg and
   holds there, which is the same picture as 0 but never snaps back mid-air. */
@keyframes smFlip {
  0%, 38% { transform: translateY(0) rotate(0deg); }
  46% { transform: translateY(-58px) rotate(140deg); }
  54% { transform: translateY(-58px) rotate(255deg); }
  62%, 100% { transform: translateY(0) rotate(360deg); }
}
/* Leaning AGAINST the direction of travel is the whole moonwalk illusion: the
   feet push left while the body slides right. Upright before the flip. */
@keyframes smLean {
  0% { transform: rotate(0deg); }
  10%, 32% { transform: rotate(-9deg); }
  38%, 100% { transform: rotate(0deg); }
}
@keyframes smBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
@keyframes smNod { 0%, 100% { transform: translateY(0) rotate(-3deg); } 50% { transform: translateY(2px) rotate(4deg); } }
@keyframes smLegL {
  0% { transform: rotate(-32deg); }
  9% { transform: rotate(16deg); }
  19% { transform: rotate(-32deg); }
  28% { transform: rotate(16deg); }
  38% { transform: rotate(-32deg); }
  44%, 58% { transform: rotate(-72deg); }
  72% { transform: rotate(24deg); }
  86% { transform: rotate(-24deg); }
  100% { transform: rotate(8deg); }
}
@keyframes smLegR {
  0% { transform: rotate(18deg); }
  9% { transform: rotate(-30deg); }
  19% { transform: rotate(18deg); }
  28% { transform: rotate(-30deg); }
  38% { transform: rotate(18deg); }
  44%, 58% { transform: rotate(-58deg); }
  72% { transform: rotate(-24deg); }
  86% { transform: rotate(24deg); }
  100% { transform: rotate(-8deg); }
}
/* Shins trail the thigh: a knee that stays locked is the tell of a puppet. */
@keyframes smShinL {
  0% { transform: rotate(34deg); }
  9% { transform: rotate(6deg); }
  19% { transform: rotate(38deg); }
  28% { transform: rotate(6deg); }
  38% { transform: rotate(34deg); }
  44%, 58% { transform: rotate(104deg); }
  72% { transform: rotate(10deg); }
  86% { transform: rotate(42deg); }
  100% { transform: rotate(14deg); }
}
@keyframes smShinR {
  0% { transform: rotate(8deg); }
  9% { transform: rotate(40deg); }
  19% { transform: rotate(8deg); }
  28% { transform: rotate(40deg); }
  38% { transform: rotate(8deg); }
  44%, 58% { transform: rotate(96deg); }
  72% { transform: rotate(40deg); }
  86% { transform: rotate(10deg); }
  100% { transform: rotate(20deg); }
}
/* Arms go overhead through the flip -- tucked limbs are what makes a rotation
   read as a backflip instead of a body being spun. */
@keyframes smArmL {
  0%, 38% { transform: rotate(34deg); }
  44%, 58% { transform: rotate(168deg); }
  72% { transform: rotate(-40deg); }
  86% { transform: rotate(46deg); }
  100% { transform: rotate(20deg); }
}
@keyframes smArmR {
  0%, 38% { transform: rotate(-34deg); }
  44%, 58% { transform: rotate(-168deg); }
  72% { transform: rotate(46deg); }
  86% { transform: rotate(-40deg); }
  100% { transform: rotate(-20deg); }
}
@keyframes smForeL {
  0% { transform: rotate(-28deg); }
  19% { transform: rotate(-62deg); }
  38% { transform: rotate(-28deg); }
  44%, 58% { transform: rotate(-14deg); }
  72% { transform: rotate(-70deg); }
  86% { transform: rotate(-20deg); }
  100% { transform: rotate(-36deg); }
}
@keyframes smForeR {
  0% { transform: rotate(28deg); }
  19% { transform: rotate(62deg); }
  38% { transform: rotate(28deg); }
  44%, 58% { transform: rotate(14deg); }
  72% { transform: rotate(20deg); }
  86% { transform: rotate(70deg); }
  100% { transform: rotate(36deg); }
}
/* Belt and braces: stickmanDance() already refuses under reduced motion, but if
   the scene ever gets built another way it must not tumble anyway. */
@media (prefers-reduced-motion: reduce) { .sm-scene { display: none; } }

/* ---------- the party crowd ----------
   Fifty more for Party Mode's hold. The SAME figure -- the sm- skeleton, limbs, leaf
   and all -- but the crowd's own choreography: nobody crosses the screen, each dancer
   holds a spot spread over the whole frozen frame and works through one of five moves,
   each move with a different flip in it. Every crowd rule is scoped under .pc-scene so
   the troupe's travel/flip/lean/bob tracks never fight these, and every keyframe name
   is pc- prefixed so it cannot collide with the troupe's sm- family. */
.pc-scene {
  /* Over the roll (92) like the raised troupe (95): the crowd dances OVER the frozen
     text, and the exit clears the floor before the roll ever moves again. */
  position: fixed; inset: 0; z-index: 94; pointer-events: none; overflow: hidden;
}
.pc-spot {
  position: absolute; width: 60px; height: 130px; margin: -65px 0 0 -30px;
  opacity: 0;
  animation: pcPop .45s ease-out var(--pc-delay, 0s) both;
}
/* The exit outranks the entrance: one class on the scene fades every dancer out
   together, whatever their own delays were doing. */
.pc-scene.pc-leaving .pc-spot { animation: pcOut .6s ease-in both; }
@keyframes pcPop { from { opacity: 0; transform: translateY(18px) scale(.4); } to { opacity: 1; transform: none; } }
@keyframes pcOut { from { opacity: 1; } to { opacity: 0; transform: translateY(24px) scale(.3); } }
/* The troupe's crossing and lean belong to the troupe; a crowd dancer stays on its
   spot. The bob keeps running -- everyone grooves between their own tricks. */
.pc-spot .sm-travel { animation: none; }
.pc-spot .sm-lean { animation: none; }
.pc-spot .sm-bob { animation: pcGroove calc(var(--pc-dur, 2s) / 2) ease-in-out var(--pc-delay, 0s) infinite alternate; }
.pc-spot .sm-fig { transform: scale(var(--pc-scale, 1)); }
/* Five moves, five different flips, each driven off its dancer's own tempo. The flips
   ride .sm-flip so they rotate about the waist, same as the troupe's backflip. */
.pc-spinjump .sm-flip { animation: pcSpinJump var(--pc-dur, 2s) cubic-bezier(.34, .12, .24, 1) var(--pc-delay, 0s) infinite; }
.pc-frontflip .sm-flip { animation: pcFrontFlip var(--pc-dur, 2s) cubic-bezier(.42, 0, .3, 1) var(--pc-delay, 0s) infinite; }
.pc-cartwheel .sm-flip { animation: pcCartwheel var(--pc-dur, 2s) ease-in-out var(--pc-delay, 0s) infinite; }
.pc-breaker .sm-flip { animation: pcBreaker var(--pc-dur, 2s) ease-in-out var(--pc-delay, 0s) infinite; }
.pc-springer .sm-flip { animation: pcSpringer var(--pc-dur, 2s) cubic-bezier(.5, -.3, .4, 1.4) var(--pc-delay, 0s) infinite; }
/* spin jump: a hop with a full turn the OTHER way from the troupe's backflip */
@keyframes pcSpinJump {
  0%, 20% { transform: translateY(0) rotate(0deg); }
  40% { transform: translateY(-46px) rotate(-160deg); }
  55% { transform: translateY(-46px) rotate(-270deg); }
  70%, 100% { transform: translateY(0) rotate(-360deg); }
}
/* front flip: tips forward into the tumble and lands tall */
@keyframes pcFrontFlip {
  0%, 15% { transform: translateY(0) rotate(0deg); }
  30% { transform: translateY(-30px) rotate(80deg); }
  50% { transform: translateY(-62px) rotate(210deg); }
  65% { transform: translateY(-24px) rotate(320deg); }
  78%, 100% { transform: translateY(0) rotate(360deg); }
}
/* cartwheel: the turn travels sideways and comes back, a wheel rather than a hop */
@keyframes pcCartwheel {
  0%, 10% { transform: translateX(0) rotate(0deg); }
  35% { transform: translateX(16px) rotate(180deg); }
  60% { transform: translateX(30px) rotate(360deg); }
  80% { transform: translateX(12px) rotate(360deg); }
  100% { transform: translateX(0) rotate(360deg); }
}
/* breaker: drops low and spins flat out -- two full turns on the floor */
@keyframes pcBreaker {
  0%, 12% { transform: translateY(0) rotate(0deg); }
  26% { transform: translateY(26px) rotate(90deg); }
  62% { transform: translateY(30px) rotate(560deg); }
  80% { transform: translateY(26px) rotate(680deg); }
  95%, 100% { transform: translateY(0) rotate(720deg); }
}
/* springer: two little bounces, then a half-flip UP that finishes on the way down */
@keyframes pcSpringer {
  0% { transform: translateY(0) rotate(0deg); }
  12% { transform: translateY(-18px) rotate(0deg); }
  24% { transform: translateY(0) rotate(0deg); }
  36% { transform: translateY(-16px) rotate(0deg); }
  48% { transform: translateY(0) rotate(0deg); }
  66% { transform: translateY(-54px) rotate(180deg); }
  84%, 100% { transform: translateY(0) rotate(360deg); }
}
@keyframes pcGroove { from { transform: translateY(0); } to { transform: translateY(-6px); } }

/* ---------- the crowd's limbs ----------
   THE BUG THIS FIXES. The limb tracks (smArmL, smForeL, smLegL, smShinL and their
   mirrors) are wired to `var(--sm-dur, 4s)` with fill `both` and NO iteration count --
   they play through once and hold. A crowd dancer never sets --sm-dur, so all eight
   limbs took the 4s fallback, ran one cycle and then froze for the rest of the party.
   The bodies were tumbling with their arms and legs welded in place, which is exactly
   the stale look.

   So the crowd gets its own set: infinite, alternating, and driven off --pc-dur so a
   dancer's joints keep the tempo the rest of its body is already dancing to. The
   SECOND segment of each limb runs at HALF the duration -- an elbow that flicks twice
   per shoulder swing is what separates a joint from a hinge. */
.pc-spot .sm-arm.sm-l { animation: pcArmL var(--pc-dur, 2s) ease-in-out var(--pc-delay, 0s) infinite alternate; }
.pc-spot .sm-arm.sm-r { animation: pcArmR var(--pc-dur, 2s) ease-in-out var(--pc-delay, 0s) infinite alternate; }
.pc-spot .sm-arm.sm-l .sm-fore { animation: pcForeL calc(var(--pc-dur, 2s) / 2) ease-in-out var(--pc-delay, 0s) infinite alternate; }
.pc-spot .sm-arm.sm-r .sm-fore { animation: pcForeR calc(var(--pc-dur, 2s) / 2) ease-in-out var(--pc-delay, 0s) infinite alternate; }
.pc-spot .sm-leg.sm-l { animation: pcLegL var(--pc-dur, 2s) ease-in-out var(--pc-delay, 0s) infinite alternate; }
.pc-spot .sm-leg.sm-r { animation: pcLegR var(--pc-dur, 2s) ease-in-out var(--pc-delay, 0s) infinite alternate; }
.pc-spot .sm-leg.sm-l .sm-shin { animation: pcShinL calc(var(--pc-dur, 2s) / 2) ease-in-out var(--pc-delay, 0s) infinite alternate; }
.pc-spot .sm-leg.sm-r .sm-shin { animation: pcShinR calc(var(--pc-dur, 2s) / 2) ease-in-out var(--pc-delay, 0s) infinite alternate; }
.pc-spot .sm-head { animation: pcNod calc(var(--pc-dur, 2s) / 3) ease-in-out var(--pc-delay, 0s) infinite alternate; }

/* the default groove: big opposed shoulder swings, elbows and knees working through */
@keyframes pcArmL { from { transform: rotate(-46deg); } to { transform: rotate(62deg); } }
@keyframes pcArmR { from { transform: rotate(58deg); } to { transform: rotate(-52deg); } }
@keyframes pcForeL { from { transform: rotate(8deg); } to { transform: rotate(96deg); } }
@keyframes pcForeR { from { transform: rotate(104deg); } to { transform: rotate(12deg); } }
@keyframes pcLegL { from { transform: rotate(-26deg); } to { transform: rotate(30deg); } }
@keyframes pcLegR { from { transform: rotate(28deg); } to { transform: rotate(-24deg); } }
@keyframes pcShinL { from { transform: rotate(6deg); } to { transform: rotate(74deg); } }
@keyframes pcShinR { from { transform: rotate(68deg); } to { transform: rotate(4deg); } }
@keyframes pcNod { from { transform: translateY(0) rotate(-5deg); } to { transform: translateY(3px) rotate(6deg); } }

/* ---------- one set of arms and legs per move ----------
   Same specificity as the block above (0-3-0) and deliberately placed after it, so
   these win on order. Only the NAME is overridden: the duration, the delay and the
   infinite-alternate all carry through, which keeps every dancer's joints locked to
   the tempo of its own body. */
/* spin jump: both arms thrown overhead, knees snapping up under the turn */
.pc-spinjump .sm-arm.sm-l { animation-name: pcArmHighL; }
.pc-spinjump .sm-arm.sm-r { animation-name: pcArmHighR; }
.pc-spinjump .sm-leg.sm-l { animation-name: pcLegTuckL; }
.pc-spinjump .sm-leg.sm-r { animation-name: pcLegTuckR; }
/* front flip: arms reach forward and fold in for the tuck */
.pc-frontflip .sm-arm.sm-l { animation-name: pcArmReachL; }
.pc-frontflip .sm-arm.sm-r { animation-name: pcArmReachR; }
.pc-frontflip .sm-leg.sm-l { animation-name: pcLegTuckL; }
.pc-frontflip .sm-leg.sm-r { animation-name: pcLegTuckR; }
/* cartwheel: arms out wide like spokes, legs scissoring past each other */
.pc-cartwheel .sm-arm.sm-l { animation-name: pcArmWideL; }
.pc-cartwheel .sm-arm.sm-r { animation-name: pcArmWideR; }
.pc-cartwheel .sm-leg.sm-l { animation-name: pcLegSplitL; }
.pc-cartwheel .sm-leg.sm-r { animation-name: pcLegSplitR; }
/* breaker: one arm planted on the floor, legs kicking out of the spin */
.pc-breaker .sm-arm.sm-l { animation-name: pcArmPlantL; }
.pc-breaker .sm-arm.sm-r { animation-name: pcArmPlantR; }
.pc-breaker .sm-leg.sm-l { animation-name: pcLegKickL; }
.pc-breaker .sm-leg.sm-r { animation-name: pcLegKickR; }
/* springer: arms pumping the bounce, knees driving underneath */
.pc-springer .sm-arm.sm-l { animation-name: pcArmPumpL; }
.pc-springer .sm-arm.sm-r { animation-name: pcArmPumpR; }
.pc-springer .sm-leg.sm-l { animation-name: pcLegDriveL; }
.pc-springer .sm-leg.sm-r { animation-name: pcLegDriveR; }

@keyframes pcArmHighL { from { transform: rotate(-150deg); } to { transform: rotate(-172deg); } }
@keyframes pcArmHighR { from { transform: rotate(154deg); } to { transform: rotate(174deg); } }
@keyframes pcArmReachL { from { transform: rotate(-104deg); } to { transform: rotate(-14deg); } }
@keyframes pcArmReachR { from { transform: rotate(102deg); } to { transform: rotate(16deg); } }
@keyframes pcArmWideL { from { transform: rotate(-84deg); } to { transform: rotate(-108deg); } }
@keyframes pcArmWideR { from { transform: rotate(86deg); } to { transform: rotate(110deg); } }
@keyframes pcArmPlantL { from { transform: rotate(-24deg); } to { transform: rotate(-8deg); } }
@keyframes pcArmPlantR { from { transform: rotate(126deg); } to { transform: rotate(166deg); } }
@keyframes pcArmPumpL { from { transform: rotate(-160deg); } to { transform: rotate(-28deg); } }
@keyframes pcArmPumpR { from { transform: rotate(164deg); } to { transform: rotate(30deg); } }
@keyframes pcLegTuckL { from { transform: rotate(-8deg); } to { transform: rotate(58deg); } }
@keyframes pcLegTuckR { from { transform: rotate(10deg); } to { transform: rotate(-56deg); } }
@keyframes pcLegSplitL { from { transform: rotate(-54deg); } to { transform: rotate(46deg); } }
@keyframes pcLegSplitR { from { transform: rotate(52deg); } to { transform: rotate(-48deg); } }
@keyframes pcLegKickL { from { transform: rotate(-70deg); } to { transform: rotate(22deg); } }
@keyframes pcLegKickR { from { transform: rotate(18deg); } to { transform: rotate(-74deg); } }
@keyframes pcLegDriveL { from { transform: rotate(-14deg); } to { transform: rotate(64deg); } }
@keyframes pcLegDriveR { from { transform: rotate(66deg); } to { transform: rotate(-12deg); } }
/* Belt and braces, same as the troupe: partyCrowd() already refuses under reduced
   motion, but a scene built any other way must not tumble either. */
@media (prefers-reduced-motion: reduce) { .pc-scene { display: none; } }
/* During Party Mode the troupe has to cross the speech, not hide behind it: the
   credits overlay sits at z-index 92 and the dance layer at 65. Raised only while the
   party is holding, so the confirmation-screen dancers keep their normal place. */
/* The paused roll. A marker for the JS; the hold itself is a frozen transform, so
   there is nothing here to animate — but it is a hook worth having, and Party Mode
   uses it to know it is holding. */
.credits-overlay.cr-paused { --cr-holding: 1; }

/* The Special Thanks wall: three columns. align-items:start so a two-line name does
   not stretch its neighbours, and a narrow-screen fallback to two then one, because
   three columns of names on a phone is three columns of hyphenation. */
.cr-names3 {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  /* Two blank lines between rows. In em rather than rem so the spacing tracks the roll's
     type size — the credits scale with the viewport, and a rem gap would stay put while
     the names around it grew. Column gap stays tight; it is the ROWS that needed air. */
  gap: 3em 1.6rem; align-items: start;
  max-width: min(880px, 92vw); margin: 0 auto;
}
.cr-name3 { display: block; text-align: center; }
/* A bracketed aside, smaller so it stays on its name's line. The grid is built on equal
   columns, so one cell wrapping to two rows pushes its whole row out of alignment. */
.cr-note { font-size: .72em; opacity: .78; }
/* Blank lines between blocks. Height is set inline in em by buildRoll. */
.cr-gap { pointer-events: none; }
@media (max-width: 760px) { .cr-names3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 440px) { .cr-names3 { grid-template-columns: 1fr; } }
/* Party Mode paints each direct child of the roll; the grid's children are one level
   deeper, so they need the gradient themselves or they render invisible. */
.party-overlay .cr-name3 {
  background-image: linear-gradient(100deg,
    #ff2d94, #ff8a3d, #ffe14d, #4dd964, #3da9ff, #a95cff, #ff2d94);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: partyFlow 6s linear infinite;
}
@media (prefers-reduced-motion: reduce) { .party-overlay .cr-name3 { animation: none; } }
/* The note is a child of a cell that is already gradient-clipped; it needs its own or
   it renders as transparent text over the roll. */
.party-overlay .cr-note {
  background-image: linear-gradient(100deg,
    #ff2d94, #ff8a3d, #ffe14d, #4dd964, #3da9ff, #a95cff, #ff2d94);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}
/* Same reason as .cr-name3/.cr-note above: the label and value are children of an
   already gradient-clipped .cr-stat row, and need their own gradient or they render
   invisible. The dotted leader between them is a border, not text -- background-clip
   does nothing for it, and it would otherwise inherit color: transparent from .cr-stat
   above and vanish, so its own colour is set explicitly instead of left at currentColor. */
.party-overlay .cr-stat-label,
.party-overlay .cr-stat-value {
  background-image: linear-gradient(100deg,
    #ff2d94, #ff8a3d, #ffe14d, #4dd964, #3da9ff, #a95cff, #ff2d94);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: partyFlow 6s linear infinite;
}
.party-overlay .cr-stat-leader { border-bottom-color: rgba(255, 255, 255, .55); }
@media (prefers-reduced-motion: reduce) {
  .party-overlay .cr-stat-label, .party-overlay .cr-stat-value { animation: none; }
}

/* ---------- Party Mode ----------
   Rainbow text over the whole roll.

   THE GRADIENT GOES ON EACH LINE, NOT THE CONTAINER. background-clip: text on the roll
   itself stretches one gradient across the entire scroll height, so the slice inside the
   viewport is a single flat colour and the rainbow never actually appears. Per line, it
   reads as a rainbow at any scroll position.

   Every gradient wraps back to its first colour, or the flow animation jumps on loop. */
.party-overlay .cr-jsdriven > * {
  background-image: linear-gradient(100deg,
    #ff2d94, #ff8a3d, #ffe14d, #4dd964, #3da9ff, #a95cff, #ff2d94);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  /* Without this the solid text paints over the gradient and nothing changes. */
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: partyFlow 6s linear infinite;
}
@keyframes partyFlow { to { background-position: 200% 0; } }
/* The essay paragraphs are children of their block, and background-clip does not
   inherit through — without their own gradient they render as transparent text on a
   dark overlay, which is to say invisible. */
.party-overlay .cr-essay {
  background-image: linear-gradient(100deg,
    #ff2d94, #ff8a3d, #ffe14d, #4dd964, #3da9ff, #a95cff, #ff2d94);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: partyFlow 6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  /* Still a rainbow, just not a moving one: removing the colour entirely would take
     the feature away from someone who only asked it to stop moving. */
  .party-overlay .cr-jsdriven > * { animation: none; }
  .party-overlay .cr-essay { animation: none; }
}

/* Twinkles: scattered across the whole screen -- see spawnPartyTwinkles() in app.js.
   No z-index: the layer is PREPENDED ahead of the rolls, so ordinary DOM paint order
   keeps every line of text on top of it without anything here needing to dodge the
   text's own position. Fixed to the viewport rather than the roll, so they hold still
   as a backdrop while the words scroll past over them. Held at opacity 0 until
   partyModeEgg() adds .show, once the scrolling names start reaching the top of the
   screen -- appearing under a wall of static names would read as a layout glitch. */
.party-twinkles {
  position: fixed; inset: 0; pointer-events: none;
  opacity: 0; transition: opacity 1.4s ease;
}
.party-twinkles.show { opacity: 1; }
.party-twinkle {
  position: fixed; top: 0; left: 0;
  width: var(--tw-s); height: var(--tw-s);
  border-radius: 50%;
  background: var(--tw-c);
  box-shadow: 0 0 calc(var(--tw-s) * 1.4) var(--tw-c), 0 0 calc(var(--tw-s) * 3.2) var(--tw-c);
  opacity: 0;
  transform: translate(var(--tw-x), var(--tw-y)) scale(.4);
  animation: partyTwinkle var(--tw-dur) ease-in-out var(--tw-delay) infinite;
}
@keyframes partyTwinkle {
  0%, 100% { opacity: 0; transform: translate(var(--tw-x), var(--tw-y)) scale(.4); }
  50% { opacity: var(--tw-peak); transform: translate(var(--tw-x), var(--tw-y)) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  /* spawnPartyTwinkles() also refuses to run at all under this preference; belt and
     braces in case a layer somehow already exists when the setting changes mid-session. */
  .party-twinkle { animation: none; opacity: var(--tw-peak); transform: translate(var(--tw-x), var(--tw-y)); }
  .party-twinkles { transition: none; }
}

body.party-dancing .sm-scene { z-index: 95; bottom: 0; height: 100%; }

/* ---------- 'heady' ----------
   Hundreds of layers of one repeated word creeping in from off screen and stacking
   until the page is genuinely black, then HETTY BWAH in pink, then back to normal.

   WHY THE LAYERS ARE DENSELY OVERLAPPED: stacking alpha alone does not reach black,
   because text has gaps — between letters, and between lines. Rows are therefore
   placed far closer together than their own line height, and each gets its own
   horizontal offset, so the gaps in one row are covered by the glyphs of the next
   four. Aligned rows would stack their gaps into permanent vertical stripes.

   WHY THERE IS ALSO A BACKGROUND FADE: even densely overlapped, glyph coverage tops
   out a hair short of solid. The layers are what you watch getting there; the
   background easing to black over the last stretch of the fill is what makes
   "totally black" actually true rather than 99% true. Said plainly rather than
   dressed up as the text doing all of it.

   pointer-events:none throughout — nothing about this should swallow a click. */
/* Modal for the same reason as .egg-overlay: it fades the whole page to solid black
   for twelve seconds, and clicking the shop through it is not a thing to preserve. */
.heady-scene {
  position: fixed; inset: 0; z-index: 95; overflow: hidden;
  background: rgba(0, 0, 0, 0);
  opacity: 1;
  transition: opacity 1.4s ease, background-color var(--blackout, 2200ms) ease-in var(--blackout-delay, 5200ms);
}
.heady-scene.run { background: rgba(0, 0, 0, 1); }
.heady-scene.gone { opacity: 0; }
.heady-row {
  position: absolute; left: 0; white-space: nowrap;
  font-family: 'Poppins', sans-serif; font-weight: 700;
  font-size: 24px; line-height: 24px; letter-spacing: -.5px;
  color: rgba(0, 0, 0, .55); will-change: transform;
  /* Off screen by more than its own width, so nothing is visible until its turn and
     the creep has somewhere to come from. */
  transform: translateX(-110%);
}
.heady-row.from-right { transform: translateX(110%); }
/* .run plus a per-row --d, so every row shares one transition definition. Slow, because
   "creep" is the word: a fast wipe reads as a glitch. */
.heady-scene.run .heady-row {
  transform: translateX(var(--x, 0px));
  transition: transform 2.6s cubic-bezier(.25, .7, .3, 1) var(--d, 0ms);
}
/* The payoff, once the screen is black. */
.heady-word {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-family: 'Poppins', sans-serif; font-weight: 700;
  font-size: clamp(2.4rem, 11vw, 8rem); letter-spacing: .06em; text-align: center;
  color: #ff2d94; text-shadow: 0 0 28px rgba(255, 45, 148, .55);
  opacity: 0; transition: opacity .55s ease;
}
.heady-scene.reveal .heady-word { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  /* No creep and no blackout ramp: it arrives black, says its piece, and leaves.
     Twelve seconds of sliding text is exactly what this setting is asking to avoid. */
  .heady-row, .heady-scene.run .heady-row { transform: none; transition: none; }
  .heady-scene { transition: opacity 1.4s ease; background: rgba(0, 0, 0, 1); }
  .heady-word { transition: none; }
}

/* ---------- KONAMI: the +30 LIVES line and its consequences ---------- */

/* The grant notice, in NES palette rather than the shop's raspberry — the point of
   the joke is that something from another era briefly got into the store. */
.egg-lives {
  font-family: 'Poppins', sans-serif; font-weight: 700;
  font-size: clamp(2.6rem, 10vw, 8rem); line-height: 1.04; letter-spacing: .06em;
  color: #ffd34d; text-shadow: 0 0 42px rgba(255, 190, 40, .8), 0 4px 0 #b3600d;
}
.egg-overlay.show .egg-lives { animation: eggPop .4s cubic-bezier(.2, 1.5, .4, 1) both; }

/* The cart row. Marked out from merchandise so nobody reads it as a charge:
   tabular digits so the count does not jitter on the way down from 30, and a
   monospace label because that is what a cheat code should look like. */
.cart-line.lives-line { border-top: 1px dashed var(--line); border-bottom: 1px dashed var(--line); }
.cart-line.lives-line .nm {
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-weight: 700; letter-spacing: .06em; color: #b3600d;
}
.cart-line.lives-line b { font-variant-numeric: tabular-nums; min-width: 2.2ch; text-align: center; }
.lives-free {
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-size: .78rem; font-weight: 700; letter-spacing: .08em; color: #1c7a4b;
}
/* Plays once, on arrival — renderCart() rebuilds the row on every life spent, and
   the class is only applied for the grant. */
.lives-line.lives-in { animation: livesIn .5s cubic-bezier(.2, 1.5, .4, 1) both; }
@keyframes livesIn {
  from { transform: scale(.7) translateY(-8px); opacity: 0; }
  to { transform: none; opacity: 1; }
}

/* ---------- "Snake? Snake!? SNAAAAAAAAKE!" ----------
   A codec transmission, not a full-screen egg overlay: the yell interrupts the shop
   without taking it over, so the cart stays visible behind it. The three beats stack
   downward and grow, because the escalation IS the joke. */
.snake-codec {
  position: fixed; inset: auto 0 0 0; z-index: 96;
  display: flex; flex-direction: column; align-items: center; justify-content: flex-end;
  gap: .18rem; padding: 1.2rem 1rem 2.2rem;
  max-height: 72vh; overflow: hidden;
  pointer-events: none; /* the shop stays usable underneath */
  background: linear-gradient(to top, rgba(4, 10, 6, .93) 20%, rgba(4, 10, 6, 0));
  transition: opacity .5s ease;
}
.snake-codec.snake-out { opacity: 0; }
.snake-codec .sn-line {
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-weight: 700; letter-spacing: .1em; text-align: center;
  color: #8bff9e; text-shadow: 0 0 18px rgba(90, 255, 130, .55);
  animation: snakeIn .28s cubic-bezier(.2, 1.5, .4, 1) both;
}
/* Five lines, each smaller than the one that follows it, because the sizes ARE the
   escalation. The ceilings are kept modest on purpose: five stacked lines at the old
   5.5rem would run off a phone screen, and the bottom line is the one that must never
   be the one that gets clipped. */
.snake-codec .sn-q { font-size: clamp(1rem, 3.4vw, 1.6rem); }
.snake-codec .sn-call { font-size: clamp(1rem, 3.2vw, 1.5rem); letter-spacing: .06em; }
.snake-codec .sn-q2 { font-size: clamp(1.2rem, 4vw, 1.9rem); }
.snake-codec .sn-alarm { font-size: clamp(1.6rem, 6vw, 2.8rem); }
.snake-codec .sn-curse {
  font-size: clamp(2.2rem, 9vw, 4.2rem); letter-spacing: .04em; color: #d6ffdd;
  text-shadow: 0 0 40px rgba(120, 255, 160, .8);
  animation: snakeIn .28s cubic-bezier(.2, 1.5, .4, 1) both, snakeShout 1.6s ease-out both;
}
/* Earlier lines dim as the next one lands, so the newest is always the brightest and
   the whole exchange still reads as a log. The last line is exempt — there is nothing
   after it to defer to, and it is the one the eye should end on. */
.snake-codec .sn-line:not(.sn-curse) {
  animation: snakeIn .28s cubic-bezier(.2, 1.5, .4, 1) both,
             snakeDim .45s ease .95s forwards;
}
@keyframes snakeDim { to { opacity: .42; } }
@keyframes snakeIn { from { transform: translateY(14px) scale(.9); opacity: 0; } }
@keyframes snakeShout {
  0% { transform: scale(1); }
  8% { transform: scale(1.12) rotate(-1deg); }
  16% { transform: scale(1.04) rotate(1deg); }
  24% { transform: scale(1.09) rotate(-.6deg); }
  100% { transform: scale(1.02); }
}
/* Reduced motion keeps the words and the audio and drops the shake — the caption is
   the accessible channel for this egg, so it must never be the part that is removed. */
@media (prefers-reduced-motion: reduce) {
  .lives-line.lives-in { animation: none; }
  .snake-codec .sn-line, .snake-codec .sn-curse { animation: none; }
  /* The dim is motion too, so with it switched off every line must be legible at
     full strength rather than stuck at whatever opacity it started on. */
  .snake-codec .sn-line:not(.sn-curse) { animation: none; opacity: .75; }
  .snake-codec .sn-still { animation: none; }
}

/* ---------- MEGA FLARE ----------
   Every delay and duration below comes from a custom property that megaFlareEgg()
   computes by summing the beat durations. Nothing here hardcodes a millisecond, so
   retiming the sequence is a one-line change in JS and the CSS follows. */
.egg-overlay.egg-flare { background: #04060c; }
.flare-stage {
  position: absolute; inset: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.flare-dragon {
  width: min(82vw, 640px); height: auto;
  filter: drop-shadow(0 0 18px rgba(74, 168, 255, .5));
  /* THE WHITE BOX. An SVG clips to its viewBox by default, and the beam scales to twenty-six
     times its own size -- so instead of expanding past the dragon it filled the SVG's rectangle
     and stopped dead at the edges, which is a hard-edged glowing box around him.
     Letting it overflow means the beam keeps going; .flare-stage is overflow:hidden, so the
     screen edge is what actually clips it, which is where a beam should stop. */
  overflow: visible;
}

/* The wireframe draws itself on. One dasharray for every stroke: the shapes are all
   short, so a single generous length reveals them at believably different rates
   instead of in lockstep, which is the look we want anyway. */
.flare-dragon polyline, .flare-dragon polygon {
  fill: none; stroke: #6fd3ff; stroke-width: 2.4; stroke-linejoin: round;
  stroke-dasharray: 620; stroke-dashoffset: 620;
  animation: flDraw var(--fl-summon) ease-out var(--fl-summon-at) forwards;
}
.flare-dragon .fl-strut { stroke: #2d6fa8; stroke-width: 1.3; }
.flare-dragon .fl-tooth { stroke: #bff4ff; stroke-width: 1.8; }
/* The charge, made visible. The orb was the only thing that changed during the longest beat of
   the sequence; now the whole dragon lifts with it, so there is something building rather than
   a dark screen with a dot on it. */
.flare-quake .flare-dragon, .flare-stage .flare-dragon {
  animation: flCharging var(--fl-charge) ease-in var(--fl-charge-at) forwards;
}
@keyframes flCharging {
  0% { filter: drop-shadow(0 0 0 rgba(120, 200, 255, 0)); transform: scale(1); }
  70% { filter: drop-shadow(0 0 18px rgba(120, 200, 255, .55)); transform: scale(1.035); }
  100% { filter: drop-shadow(0 0 34px rgba(180, 230, 255, .85)); transform: scale(1.06); }
}
.flare-dragon .fl-eye {
  stroke: #ff7a4a; stroke-width: 2.2;
  animation: flDraw var(--fl-summon) ease-out var(--fl-summon-at) forwards,
             flEye 1.1s ease-in-out var(--fl-charge-at) infinite alternate;
}
@keyframes flDraw { to { stroke-dashoffset: 0; } }
@keyframes flEye {
  from { fill: rgba(255, 122, 74, .15); filter: none; }
  to { fill: rgba(255, 190, 90, .85); filter: drop-shadow(0 0 12px rgba(255, 150, 60, .9)); }
}

/* The jaws part on a hinge at the back of the mandible, not at the chin — rotating
   about the chin swings the whole jaw off the face. */
/* IT DROPS, IT DOES NOT SWING. The jaw used to rotate 15 degrees about a single point, and the
   head is mirror-symmetric about x=200 -- so a rotation lifts one side of the mouth and lowers
   the other, and it opened visibly wider on the left than the right. Straight down is the only
   motion that opens a symmetric shape evenly. A little vertical stretch stands in for the swing
   that is gone, so it still reads as a jaw rather than a sliding panel. */
.fl-jaw {
  transform-box: fill-box; transform-origin: top center;
  animation: flJaw var(--fl-charge) cubic-bezier(.3, .9, .3, 1) var(--fl-charge-at) forwards;
}
@keyframes flJaw { to { transform: translateY(20px) scaleY(1.14); } }

/* The light: nothing, then a swell that outgrows the head. */
.fl-orb {
  transform-box: fill-box; transform-origin: center; transform: scale(0);
  animation: flOrb var(--fl-charge) cubic-bezier(.55, 0, 1, .5) var(--fl-charge-at) forwards;
}
@keyframes flOrb {
  0% { transform: scale(0); opacity: .7; }
  70% { transform: scale(1.5); opacity: 1; }
  100% { transform: scale(2.6); opacity: 1; }
}
/* The release comes straight down the barrel at the viewer, so it is an expansion
   rather than a sweep — no direction for the eye to follow and get ahead of. */
.fl-beam {
  transform-box: fill-box; transform-origin: center; transform: scale(0); opacity: 0;
  animation: flBeam var(--fl-beam) cubic-bezier(.2, .8, .2, 1) var(--fl-beam-at) forwards;
}
@keyframes flBeam {
  0% { transform: scale(0); opacity: 0; }
  12% { transform: scale(3); opacity: 1; }
  100% { transform: scale(26); opacity: 0; }
}
.flare-flash {
  position: absolute; inset: 0; background: #fff; opacity: 0; pointer-events: none;
  animation: flFlash var(--fl-beam) ease-out var(--fl-beam-at) forwards;
}
/* Softened. It hit pure white at full opacity, which on a near-black screen is a single very
   hard flash; the beam still reads as a detonation at two thirds. */
@keyframes flFlash {
  0% { opacity: 0; }
  9% { opacity: .68; }
  38% { opacity: .34; }
  100% { opacity: 0; }
}
/* Sized for NINETEEN CHARACTERS, not ten. The old ceiling of 5rem at .18em tracking
   was set when this line read "MEGA FLARE"; the current phrase at those numbers is
   about twice the width of a laptop screen. The tracking came down with the size
   because tracking costs width per character and this line has nearly twice as many,
   and it is allowed to wrap: two lines is a fine look for the payoff, a clipped line
   is not. */
.flare-word {
  position: absolute; left: 0; right: 0; bottom: 12vh; text-align: center;
  max-width: 90vw; margin: 0 auto; line-height: 1.08;
  font-family: 'Poppins', sans-serif; font-weight: 700;
  font-size: clamp(1.7rem, 7.5vw, 3.6rem); letter-spacing: .08em;
  color: #dff4ff; text-shadow: 0 0 34px rgba(110, 200, 255, .9);
  opacity: 0;
  animation: flWord var(--fl-scorch) ease-out var(--fl-beam-at) forwards;
}
@keyframes flWord {
  0% { opacity: 0; transform: scale(.86); }
  14% { opacity: 1; transform: scale(1.04); }
  70% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1); }
}

/* The quake.
 *
 * IT USED TO SHAKE <body> AT ROUGHLY ELEVEN HERTZ, in hard two-step jumps. Two things wrong with
 * that. Sliding the whole page back and forth exposes the backdrop down each edge, and at that
 * rate the bands strobe -- which is what read as "seizure inducing side panels". And 11Hz hard
 * alternation sits in the middle of the photosensitivity band, which is not something to ship
 * for a joke about a dragon.
 *
 * It moves the STAGE now, not the page: the stage is inset inside a full-screen overlay, so
 * there is no edge to expose. Smooth rather than stepped, and slow enough to read as a rumble
 * rather than a flicker. Still removed by JS rather than left to expire -- a lingering transform
 * on an ancestor re-parents every position:fixed element on the page. */
.flare-quake .flare-stage { animation: flQuake .34s ease-in-out infinite; }
@keyframes flQuake {
  0% { transform: translate3d(-3px, 1px, 0); }
  50% { transform: translate3d(3px, -2px, 0); }
  100% { transform: translate3d(-2px, 2px, 0); }
}

/* Reduced motion keeps the dragon, the light, the word and every sound. What goes is
   the shaking and the draw-on: the parts that are motion for its own sake. */
@media (prefers-reduced-motion: reduce) {
  .flare-dragon polyline, .flare-dragon polygon { animation: none; stroke-dashoffset: 0; }
  .flare-dragon .fl-eye { animation: none; fill: rgba(255, 170, 80, .6); }
  .fl-jaw { animation: none; transform: rotate(15deg) translateY(14px); }
  .flare-quake .flare-stage { animation: none; }
  .fl-orb { animation-timing-function: ease-out; }
}
.flare-stage.fl-still .fl-beam { animation-duration: var(--fl-beam); }

/* The fifth beat, which the timing arithmetic always allowed for and nothing was
   spending: after the white-out the stage does not snap back to flat black, it holds
   a moment of ember light while the word sits there. Anchored at --fl-scorch-at, so
   it begins exactly where the beam beat ends. */
.flare-embers {
  position: absolute; inset: 0; opacity: 0; pointer-events: none;
  background: radial-gradient(circle at 50% 62%, rgba(255, 138, 60, .55), rgba(120, 30, 8, .3) 45%, transparent 72%);
  animation: flEmbers var(--fl-scorch) ease-out var(--fl-scorch-at) forwards;
}
@keyframes flEmbers {
  0% { opacity: .95; }
  55% { opacity: .4; }
  100% { opacity: 0; }
}

/* ---------- the ragdoll logo ----------
   Grab the logo and it leaves its socket; the emoji behind it was always there. */

/* touch-action:none is what lets a finger drag the logo instead of scrolling the page.
   Scoped to the brand button, which is a small target, so scrolling everywhere else is
   untouched. */
.brand.logo-draggable { touch-action: none; }
/* WebKit reads its own property for native image dragging, and the img is what the
   pointer actually lands on. Without this, pressing the logo and moving hands the gesture
   to the browser's drag-and-drop and the physics never sees it. */
.brand.logo-draggable img { -webkit-user-drag: none; user-select: none; -webkit-user-select: none; }
/* The logo sits ABOVE the socket and keeps its own stacking context, so pulling it away
   uncovers rather than reorders. */
.brand img { position: relative; z-index: 1; }
.logo-socket {
  position: absolute; inset: 0; z-index: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 34px; line-height: 1; pointer-events: none;
  opacity: 0; /* JS drives this from displacement, so the reveal tracks the pull */
  cursor: default;
  transition: opacity .12s linear;
  user-select: none;
}
/* No transition on the logo itself while it is being dragged: the physics writes a new
   transform every frame, and a transition would fight it and smear the swing. */
/* Live only while the logo is sailing home and the emoji is actually visible — JS owns
   the class. Poking it sends the return erratic. */
.logo-socket.socket-live { pointer-events: auto; cursor: pointer; }
.brand.logo-ragdolling img { transition: none; cursor: grabbing; }
.brand.logo-ragdolling { cursor: grabbing; }
.brand:active { cursor: grabbing; }

@media (max-width: 640px) {
  /* The logo is 38px here rather than 62px, so the socket emoji comes down with it. */
  .logo-socket { font-size: 22px; }
}
@media (prefers-reduced-motion: reduce) {
  /* The drag is direct manipulation and stays; what goes is the swing and the journey
     home, both handled in JS. The socket still reveals -- it is the point of the
     feature, and fading it is not motion anybody needs protecting from. */
  .logo-socket { transition: none; }
}

/* ---------- the wall ----------
   What a real hit looks like: spawnSmackFx/spawnGlassShards (common.js) drop one of these
   short-lived layers at the logo's current position -- the same translate3d(x, y, 0) offset
   paint() already uses -- and let it clean itself up. Nothing here is threaded through the
   physics loop; both finish well inside the recovery window on their own. */
.smack-fx {
  position: absolute; left: 0; top: 0; width: 0; height: 0;
  pointer-events: none; z-index: 4;
}
.smack-particle {
  position: absolute; left: 0; top: 0; font-size: 1.15rem; line-height: 1;
  transform: translate(-50%, -50%);
  animation: logoHitParticle var(--pd, 700ms) cubic-bezier(.22, .9, .3, 1) forwards;
}
@keyframes logoHitParticle {
  0% { transform: translate(-50%, -50%) translate(0, 0) rotate(0deg); opacity: 1; }
  100% {
    transform: translate(-50%, -50%) translate(var(--px), var(--py)) rotate(var(--pr));
    opacity: 0;
  }
}
/* "-20 HP", small, red, and gone in under a second -- the one number every persona's smack
   line agrees on (RAGDOLL.SMACK_HP), floating up and fading rather than just appearing and
   vanishing. */
.smack-hp {
  position: absolute; left: 50%; top: -8px; transform: translate(-50%, 0);
  font-family: 'Poppins', sans-serif; font-weight: 800; font-size: .82rem;
  color: #e0263f; text-shadow: 0 1px 0 rgba(255, 255, 255, .55), 0 0 6px rgba(224, 38, 63, .35);
  white-space: nowrap; pointer-events: none;
  animation: logoHitHp 850ms ease-out forwards;
}
@keyframes logoHitHp {
  0% { transform: translate(-50%, 0); opacity: 1; }
  100% { transform: translate(-50%, -34px); opacity: 0; }
}
/* The fifth hit: glass triangles instead of stars-and-anger, flung the same way. clip-path
   turns a plain square into a shard; the gradient gives it something to catch the light
   with instead of reading as a flat coloured chip. */
.smack-shard {
  position: absolute; left: 0; top: 0;
  width: var(--ss, 12px); height: var(--ss, 12px);
  background: linear-gradient(135deg, rgba(255, 255, 255, .95), rgba(190, 220, 235, .55));
  border: 1px solid rgba(255, 255, 255, .7);
  clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
  transform: translate(-50%, -50%);
  animation: logoHitShard var(--pd, 600ms) cubic-bezier(.3, .1, .3, 1) forwards;
}
@keyframes logoHitShard {
  0% { transform: translate(-50%, -50%) translate(0, 0) rotate(0deg) scale(1); opacity: 1; }
  100% {
    transform: translate(-50%, -50%) translate(var(--px), var(--py)) rotate(var(--pr)) scale(.4);
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  /* Belt and braces: spawnSmackFx and the cause === 'smack' call to spawnGlassShards in
     banish() both already check this themselves and skip spawning anything at all -- the
     fall itself is a separate, existing reduced-motion branch (banish()'s own still()
     check). This is only here in case either one is ever called without that guard. */
  .smack-particle, .smack-shard { animation: none; opacity: 0; }
  .smack-hp { animation: none; }
}

/* ---------- the painting logos ----------
   Black artwork at 30%, assorted sizes, tumbling in three dimensions and leaving light behind
   them in the shape of their own outline. */

/* The stage. perspective is what makes rotateX/rotateY read as depth rather than as squashing,
   and it lives HERE rather than on each copy so every logo and every deposit shares one vanishing
   point — otherwise each element gets its own and the scene stops agreeing with itself. */
.screensaver { perspective: 1100px; }

.ss-fall {
  position: absolute; top: 0; left: 0;
  width: var(--ss-w);
  margin-left: calc(var(--ss-x) - (var(--ss-w) / 2));
  pointer-events: none; will-change: transform;
  transform-style: preserve-3d;
  animation: ssFall var(--ss-dur) linear infinite;
  animation-delay: var(--ss-delay);
}
/* ONE AXIS PER ELEMENT. All three of these animate `transform`, and CSS does not add
   transforms from separate animations -- the last one declared wins outright. Stacked on a
   single element, ssSpin and ssFlip did nothing at all and every copy was only ever rotating
   about Y. Nesting them is the same fix the dancers, the ragdoll and the fall itself needed. */
.ss-roll { display: block; transform-style: preserve-3d; will-change: transform;
  animation: ssRoll var(--ss-roll) linear infinite; animation-delay: var(--ss-delay); }
.ss-flip { display: block; transform-style: preserve-3d; will-change: transform;
  animation: ssFlip var(--ss-flip) linear infinite; animation-delay: var(--ss-delay); }
.ss-fall img {
  /* WIDTH ONLY. The artwork is not square, so pinning both axes would stretch it differently at
     every random size; height:auto keeps every copy the same shape. */
  width: 100%; height: auto; display: block;
  opacity: .3;
  will-change: transform;
  transform-style: preserve-3d;
  animation: ssSpin var(--ss-spin) linear infinite;
  animation-delay: var(--ss-delay);
}
/* Three timings, deliberately unrelated: on matching periods they resolve into a single tidy
   wobble, and the point is that it never holds the same attitude twice.

   ALL LINEAR, INCLUDING THE FLIP. It was ease-in-out, which on an infinite rotation decelerates
   into 360deg and accelerates out of 0deg -- the same angle, so nothing jumps, but the motion
   visibly stalls once per cycle. A continuous tumble has to be linear or it pulses. */
@keyframes ssSpin { to { transform: rotateZ(360deg); } }
@keyframes ssFlip { to { transform: rotateX(360deg); } }
@keyframes ssRoll { to { transform: rotateY(360deg); } }
@keyframes ssFall {
  from { transform: translateY(-150%); }
  to { transform: translateY(250vh); }
}

/* A deposit. NOT a faded copy of the logo: the real artwork, recoloured to any hue at all by a
   zero-blur drop-shadow (see startSsTrails in common.js for why -- mask-image looked right
   locally and then rendered nothing against the real, cross-origin artwork). JS writes the
   transform it was standing in when it was laid down, so a mid-flip stamp lands foreshortened.
   height:auto keeps every copy's own aspect ratio regardless of --ss-w, same as .ss-fall img. */
.ss-stamp {
  position: absolute; left: 0; top: 0;
  width: var(--ss-w); height: auto; display: block;
  margin-left: calc(var(--ss-x) - (var(--ss-w) / 2));
  pointer-events: none;
  opacity: 0;
  /* Only opacity animates. The transform is written once and never touched again, so the
     browser can promote each deposit and then leave it alone. */
  animation: ssStamp 2800ms linear forwards;
  will-change: opacity;
  mix-blend-mode: screen; /* light on a dark screen, so overlapping deposits add up */
}
/* Short of full strength, and it fades linearly rather than easing out -- an ease leaves most
   of the trail sitting at nearly full strength and then dropping away, which reads as a shape
   rather than a wake. Pulled down further from the first pass, which read as glary rather than
   ghostly at full deposits; the glow (JS writes a matching drop-shadow alongside the fill
   colour) still carries most of the "light" of it, and overlapping deposits build up into a
   wake rather than flashing in individually at full brightness. */
@keyframes ssStamp {
  0% { opacity: .32; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  /* The screensaver refuses to start under reduced motion at all, so this is belt and braces —
     and the trail is not started in JS either. */
  .ss-fall, .ss-fall img, .ss-stamp { animation: none; }
}

/* ---------- the held discount, in the cart ----------
   Shown on the main page now, not only at checkout. Green like the free-shipping badge,
   because it is the same kind of news; the estimated total under it is plain, because it is
   an estimate and should not look like the authoritative figure. */
.cart-promo {
  margin-top: .35rem; font-size: .8rem; font-weight: 700;
  letter-spacing: .04em; color: #1c8a4f;
}
.cart-promo.pending { color: var(--muted); font-weight: 600; }
.cart-promo-total { margin-top: .15rem; font-size: .82rem; color: var(--muted); }
.cart-promo-total b { color: var(--ink); }

/* The station switcher, refused while the secret station is on. A brief shake rather than a
   dead tap: a control that silently ignores you reads as broken, one that flinches reads as
   locked. */
.now-playing.np-locked { animation: npLocked .55s cubic-bezier(.36, .07, .19, .97); }
@keyframes npLocked {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-3px); }
  40%, 60% { transform: translateX(3px); }
}
@media (prefers-reduced-motion: reduce) { .now-playing.np-locked { animation: none; } }

/* DEATH: the logo got smacked apart and the pill stops helping for exactly as long as it is
   gone -- see logoDead/beginDeath/endDeath in muzak.js. pointer-events: none on the whole
   widget is simpler and more robust than guarding volume, the genre picker, and mute
   individually: nothing under it can be reached, full stop, and a future control added here
   inherits the lockout for free instead of needing its own guard remembered. */
.now-playing.np-dead { pointer-events: none; opacity: .55; }

/* ============================ the intro screens ============================
   One per tab, chosen at random, clicked away to reveal the age gate underneath. It sits ON TOP
   of the gate rather than replacing it, so nothing about age verification depends on a joke. */
.intro-screen {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; cursor: pointer; opacity: 0;
  transition: opacity .25s ease;
  -webkit-tap-highlight-color: transparent;
  /* Inert until revealed. It is built at opacity 0 and shown a frame later, so between those
     two moments it is an invisible sheet over the whole page -- the exact trap that once turned
     one thrown error into a dead shop. Caught here by the sweep that was written after it. */
  pointer-events: none;
}
.intro-screen.intro-in { opacity: 1; pointer-events: auto; }
.intro-screen > * { pointer-events: none; } /* the whole screen is the button */

/* --- the five exits. Each belongs to one variant, so leaving is part of the joke. --- */
/* One exit, shared. Three different physics made the screens feel unrelated rather than like
   three faces of one gag; they all tip and fall off the bottom now. */
.exit-crash { animation: introCrash 1.1s cubic-bezier(.6,-.28,.74,.05) forwards; }
@keyframes introCrash {
  0% { transform: none; }
  14% { transform: translateY(-26px) rotate(-1.5deg); }
  100% { transform: translateY(130vh) rotate(9deg); }
}
@media (prefers-reduced-motion: reduce) {
  .intro-screen.intro-out { animation: none !important; opacity: 0; }
}

/* ---------------------------------------------------------------- 1. GeoCities */
.intro-geo { background: #000080; font-family: 'Times New Roman', Times, serif; }
.geo-page { width: 100%; max-width: 900px; background: #c0c0c0; border: 3px ridge #fff;
  padding: 4px; max-height: 100%; overflow: hidden; }
.geo-marquee { display: block; background: #ff0; color: #f0f; font-weight: 700; font-size: 15px;
  border: 2px inset #808080; padding: 2px 0; }
.geo-table { width: 100%; border-collapse: collapse; }
.geo-side { width: 96px; vertical-align: top; padding: 6px 4px; }
.geo-main { vertical-align: top; padding: 6px 10px; background: #fff; border: 2px inset #808080; }
.geo-h1 { font-size: 26px; margin: 4px 0; color: #008000; text-align: center;
  text-shadow: 2px 2px 0 #0f0; }
.geo-blink { text-align: center; color: red; font-weight: 700; margin: 2px 0;
  animation: geoBlink 1s steps(1) infinite; }
@keyframes geoBlink { 50% { opacity: 0; } }
.geo-p { font-size: 14px; line-height: 1.35; margin: 8px 0; }
.geo-links { list-style: none; padding: 0; margin: 6px 0; }
.geo-links a { color: #00f; text-decoration: underline; font-size: 14px; }
.geo-small { font-size: 11px; color: #555; }
/* The "GIF" ads: keyframed CSS rather than actual GIFs, so they cost nothing and still hurt. */
.geo-ad { height: 62px; margin-bottom: 8px; display: flex; align-items: center;
  justify-content: center; text-align: center; font-weight: 700; font-size: 13px;
  border: 2px outset #fff; color: #fff; }
.geo-ad-a { background: #f0f; animation: geoAd .4s steps(2) infinite; }
.geo-ad-b { background: #0a0; animation: geoAd2 .6s steps(2) infinite; }
.geo-ad-c { background: #f60; animation: geoAd .5s steps(2) infinite; }
.geo-ad-d { background: #06c; animation: geoAd2 .35s steps(2) infinite; }
@keyframes geoAd { 50% { background: #ff0; color: #000; } }
@keyframes geoAd2 { 50% { background: #f00; color: #ff0; } }
.geo-hits { background: #000; color: #0f0; font-family: monospace; font-size: 11px;
  text-align: center; padding: 4px 0; border: 1px solid #0f0; }
.geo-jk { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-family: 'Comic Sans MS', 'Poppins', sans-serif; font-size: clamp(3rem, 18vw, 12rem);
  font-weight: 700; color: #fff; text-shadow: 0 0 30px #0ff; z-index: 2; }

/* --------------------------------------------------------------- 2. newspaper */
.intro-news { background: #d8d2c2; }
/* It got a good deal longer. On a short screen the tail would simply be cut off, so it scrolls
   -- and because the whole overlay is the dismiss button, the scroll has to be allowed to take
   the touch without also counting as a tap. */
.news-sheet { width: 100%; max-width: 880px; max-height: 100%; overflow-y: auto;
  -webkit-overflow-scrolling: touch; pointer-events: auto;
  background: #f4f0e4; padding: clamp(.9rem, 3vw, 2rem);
  box-shadow: 0 24px 60px rgba(0,0,0,.4); font-family: 'Times New Roman', Times, serif;
  color: #1a1a1a; transform: rotate(-.4deg); }
.news-masthead { text-align: center; }
.news-masthead h1 { font-size: clamp(1.6rem, 7vw, 3.4rem); margin: .1rem 0; letter-spacing: .04em;
  font-weight: 900; }
.news-rule { display: block; height: 3px; background: #1a1a1a; }
.news-dateline { display: flex; justify-content: space-between; gap: .5rem; font-size: .62rem;
  letter-spacing: .12em; text-transform: uppercase; border-bottom: 1px solid #1a1a1a;
  padding: .25rem 0; margin-bottom: .7rem; }
.news-hed { font-size: clamp(1.3rem, 6vw, 2.6rem); text-align: center; margin: .3rem 0;
  font-weight: 900; line-height: 1.05; }
.news-dek { font-size: clamp(.72rem, 2.4vw, .95rem); text-align: center; font-weight: 400;
  font-style: italic; margin: 0 0 .8rem; border-bottom: 1px solid #1a1a1a; padding-bottom: .6rem; }
.news-cols { column-count: 2; column-gap: 1.4rem; column-rule: 1px solid #b9b09a;
  font-size: clamp(.7rem, 2.1vw, .82rem); line-height: 1.5; text-align: justify; }
.news-cols p { margin: 0 0 .6rem; }
.news-drop { float: left; font-size: 2.6em; line-height: .78; padding-right: .08em; font-weight: 700; }
.news-src { font-style: italic; font-size: .9em; color: #5a5348; }
.news-sub { margin: 1rem 0 .35rem; font-size: clamp(.78rem, 2.6vw, 1rem); font-weight: 900;
  letter-spacing: .06em; border-top: 2px solid #1a1a1a; border-bottom: 1px solid #1a1a1a;
  padding: .3rem 0; text-align: center; break-inside: avoid; }
.news-foot { margin-top: .8rem; border-top: 2px solid #1a1a1a; padding-top: .4rem;
  text-align: center; font-size: .62rem; letter-spacing: .14em; text-transform: uppercase; }
@media (max-width: 640px) { .news-cols { column-count: 1; } }

/* --------------------------------------------------------- 5. School of Athens */
.intro-athens { background: #1a1409; }
.athens-wrap { position: absolute; inset: 0; }
.athens-fallback, .athens-img { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; }
.athens-fallback svg { width: 100%; height: 100%; display: block; }
/* The painting covers the drawn stand-in when it loads; if Wikimedia does not answer, the
   onerror handler hides it and the pastiche underneath is simply what you see. */
.athens-img { z-index: 1; }
.athens-img.athens-gone { display: none; }
.athens-veil { position: absolute; inset: 0; z-index: 2;
  background: radial-gradient(circle at 50% 45%, rgba(0,0,0,0) 30%, rgba(0,0,0,.55) 100%); }
.athens-logo { position: absolute; z-index: 3; left: 50%; top: 46%;
  transform: translate(-50%, -50%); width: min(46vw, 380px); height: auto;
  filter: drop-shadow(0 10px 40px rgba(0,0,0,.85)) drop-shadow(0 0 18px rgba(255,240,200,.5)); }
.athens-plaque { position: absolute; z-index: 3; left: 50%; bottom: 8vh; transform: translateX(-50%);
  font-family: 'Times New Roman', Times, serif; letter-spacing: .3em; color: #f3e7c8;
  font-size: clamp(.6rem, 2.6vw, 1rem); text-shadow: 0 2px 12px #000;
  /* No nowrap: on a narrow phone a plaque that cannot wrap runs off both edges instead. */
  max-width: 92vw; text-align: center; }

/* Black from the first paint. The boot screen cannot exist until the body does, so for those
   few frames the page is blacked out by a class the head script sets on <html>. visibility is
   used rather than opacity because it is inherited AND overridable -- the boot screen turns
   itself back on without needing to sit outside the body. */
html.boot-pending { background: #000; }
html.boot-pending body { visibility: hidden; }
html.boot-pending body .boot-screen { visibility: visible; }

/* ============================== the boot screen ==============================
   Black, the red mark fading up over two seconds, then breathing. Twinkles and two slow lens
   flares drift behind it in the age gate's own plum and ember, so the two screens read as one
   place rather than two. It sits ABOVE the gate and removes itself on click; nothing about age
   verification depends on it. */
.boot-screen {
  position: fixed; inset: 0; z-index: 200; overflow: hidden;
  /* column, not row: .boot-logo and .boot-prompt are the only non-absolutely-positioned
     children, so stacking them in a column is what puts the prompt directly UNDER the
     mark rather than beside it. The flares/sparks/ticker are all position:absolute and
     unaffected. */
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: #000; cursor: pointer;
  opacity: 1;
  /* Inert until revealed — same rule as every other full-viewport layer here. It starts opaque
     rather than transparent, so it is live immediately and this is simply explicit. */
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
}
/* OUT IN TWO STAGES. A single fade would cross-dissolve this screen and the gate, and for half
   a second both would be on the frame at once. The contents leave first, against the black they
   were always on; then the black itself lifts. */
.boot-screen.boot-blacking > * { opacity: 0; transition: opacity 1s ease; }
.boot-screen.boot-blacking { pointer-events: none; }
.boot-screen.boot-out { opacity: 0; transition: opacity 1s ease; }

.boot-logo {
  position: relative; z-index: 3;
  width: min(72vw, 520px); height: auto;
  opacity: 0;
  /* Two seconds up, then a slow breath. Separate animations on separate properties so neither
     overwrites the other — opacity fades in once and stops, transform keeps going. */
  /* LINEAR, NOT EASE. `ease` spends its first half barely moving and then rushes the second --
     over two seconds that reads as nothing, nothing, nothing, POP. An even ramp is the only one
     that actually looks like a fade at this duration. */
  animation: bootFade 2s linear forwards, bootPulse 4.6s ease-in-out 2s infinite;
  filter: drop-shadow(0 0 34px rgba(214, 92, 96, .45));
}
@keyframes bootFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes bootPulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.045); }
}

/* ---------- the boot-screen prompt ----------
 *
 * One element, two possible messages: production shows the device-aware "tap/click
 * to continue" instruction; demo mode overwrites it with "DEMO MODE" once
 * /api/config confirms (see boot() in intro.js). Either way it arrives the same
 * way the demo badge always did -- a single fade, once, timed to land just after
 * the mark itself has faded up -- and it stays fully visible from then on; it is
 * never invisible again after that first arrival. Letters exist as individual
 * spans only so the periodic "pop" (armPromptPop, in intro.js) can stagger itself
 * across them -- the initial arrival above does not use that stagger at all.
 * Decorative and disabled for assistive tech -- role="button" + aria-label on
 * .boot-screen itself already says what this control does. */
.boot-prompt {
  position: relative; z-index: 3; margin-top: 2.6rem;
  display: flex; flex-wrap: wrap; justify-content: center; gap: .32em; perspective: 700px;
  /* A runway-show title card, not a button label: light weight, wide open tracking,
     sized up from the old small print so it reads as a second billboard line under
     the mark rather than a caption underneath it. */
  font-family: 'Poppins', sans-serif; font-weight: 500; letter-spacing: .1em;
  font-size: clamp(1.05rem, 3.4vw, 1.65rem); text-transform: uppercase;
  color: #fff8ec; opacity: 0;
  animation: bootFlareIn 1s ease 2.2s forwards;
}
.bc-ch {
  display: inline-block; transform-style: preserve-3d;
  text-shadow: 0 0 6px #fff, 0 0 14px #f4e3c8, 0 0 26px #d65c60, 0 0 42px #b57ee8;
  animation: bootPromptGlow 2.6s ease-in-out 2.2s infinite alternate;
}
@keyframes bootPromptGlow {
  from { text-shadow: 0 0 6px #fff, 0 0 14px #f4e3c8, 0 0 26px #d65c60, 0 0 42px #b57ee8; }
  to   { text-shadow: 0 0 10px #fff, 0 0 24px #f4e3c8, 0 0 44px #d65c60, 0 0 72px #b57ee8; }
}
/* The pop: text that is already visible bulges toward the viewer, letter by letter, and
   settles back to rest -- opacity is untouched, so nothing here can make it disappear.
   animation-delay is set inline (--bc-delay) at render time, the same stagger mechanism
   the old ripple-in used, just spent on a round trip instead of a one-way arrival. */
.boot-prompt.bc-pop .bc-ch {
  animation: bootPromptGlow 2.6s ease-in-out infinite alternate,
             bootPromptPop .85s cubic-bezier(.22, .9, .3, 1) both;
  animation-delay: 0s, var(--bc-delay, 0ms);
}
@keyframes bootPromptPop {
  0%   { transform: translateZ(0) rotateX(0deg); }
  45%  { transform: translateZ(30px) rotateX(-9deg); } /* the overshoot toward the viewer */
  100% { transform: translateZ(0) rotateX(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  /* The words are instructions, not decoration, so reduced motion still gets them --
     just parked at rest immediately, with no fade-in, no 3D pop, no glow pulse. */
  .boot-prompt { opacity: 1; animation: none; }
  .bc-ch {
    transform: none; animation: none !important;
    text-shadow: 0 0 8px #fff, 0 0 20px #f4e3c8, 0 0 36px #d65c60;
  }
}

/* The twinkles. Position, size, delay and duration all come from JS — twenty-two hand-written
   rules would be unreadable and would repeat visibly. */
.boot-spark {
  position: absolute; left: var(--x); top: var(--y);
  width: var(--s); height: var(--s); border-radius: 50%;
  opacity: 0; z-index: 2;
  animation: bootTwinkle var(--d) ease-in-out infinite; animation-delay: var(--delay);
}
.boot-spark-0 { background: #d65c60; box-shadow: 0 0 10px 2px rgba(214, 92, 96, .8); }
.boot-spark-1 { background: #b57ee8; box-shadow: 0 0 10px 2px rgba(181, 126, 232, .75); }
.boot-spark-2 { background: #f4e3c8; box-shadow: 0 0 10px 2px rgba(244, 227, 200, .7); }
@keyframes bootTwinkle {
  0%, 100% { opacity: 0; transform: scale(.5); }
  50% { opacity: .95; transform: scale(1); }
}

/* Two lens flares, drifting on different periods so they never line up into one shape. */
/* Depth order, bottom to top: flares, ticker, sparks, mark. */
.boot-flare { position: absolute; border-radius: 50%; filter: blur(48px); z-index: 0; }
/* THEY USED TO BE THERE FROM THE FIRST FRAME, which meant the screen opened on a wash of
   colour and the logo faded up into something already happening. They arrive with the pulse
   instead: two seconds of nothing but black, the mark, and then the room lighting around it.
   The drift and the fade are separate animations on separate properties, so neither replaces
   the other -- transform for the movement, opacity for the arrival. */
.boot-flare { opacity: 0; }
.boot-flare-a {
  width: 46vmax; height: 46vmax; left: -12vmax; top: -14vmax;
  background: radial-gradient(circle, rgba(214, 92, 96, .40), rgba(214, 92, 96, 0) 68%);
  animation: bootDriftA 19s ease-in-out infinite, bootFlareIn 3.2s ease 2s forwards;
}
.boot-flare-b {
  width: 38vmax; height: 38vmax; right: -10vmax; bottom: -12vmax;
  background: radial-gradient(circle, rgba(181, 126, 232, .34), rgba(181, 126, 232, 0) 68%);
  animation: bootDriftB 26s ease-in-out infinite, bootFlareIn 3.6s ease 2.3s forwards;
}
@keyframes bootFlareIn { to { opacity: 1; } }
@keyframes bootDriftA {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50% { transform: translate(9vmax, 7vmax) scale(1.16); }
}
@keyframes bootDriftB {
  0%, 100% { transform: translate(0, 0) scale(1.1); }
  50% { transform: translate(-8vmax, -6vmax) scale(.94); }
}

@media (prefers-reduced-motion: reduce) {
  /* The mark still arrives and the screen still works; what goes is the drifting and the
     breathing. */
  .boot-logo { animation: bootFade 2s linear forwards; }
  .boot-spark { animation: none; }
  /* The flares still have to arrive, or reduced motion leaves them invisible for good. */
  .boot-flare { animation: bootFlareIn 3.2s ease 2s forwards; }
  .boot-spark { opacity: .7; }
  /* The strip still has to appear, or reduced motion leaves it invisible for good -- the fade
     in is what makes it visible at all. */
  .boot-ticker { animation: bootTickerIn 1.1s ease 5s forwards; }
}

/* The boot ticker: one line of text crawling in from off the right, and nothing else.
 *
 * IT WAS DOING FAR TOO MUCH. Six copies of the phrase at 66rem, each carrying a 44px text-shadow,
 * is an enormous area to blur and repaint every frame -- that was the resource cost. On top of
 * that the strip itself was animating a transform (the entrance) while the track inside animated
 * another (the loop), and a transformed absolutely-positioned parent becomes the containing block
 * for its children, so the two fought over where "pinned to the bottom" actually was. Hence the
 * jumping.
 *
 * One element moves. One copy of the text. No shadow. It enters from off the right, crosses, and
 * comes round again -- which is what a crawl is. */
.boot-ticker {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  overflow: hidden; pointer-events: none;
  padding: clamp(.4rem, 1.4vh, .9rem) 0;
  background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .55));
  /* Opacity only. Nothing here may animate transform, or it becomes the containing block for the
     line inside and the pinning stops meaning anything. */
  opacity: 0;
  animation: bootTickerIn 1.1s ease 5s forwards;
}
@keyframes bootTickerIn { to { opacity: 1; } }

.boot-ticker-line {
  display: block; white-space: nowrap; width: max-content;
  font-family: 'Poppins', sans-serif;
  font-weight: 800;
  font-size: clamp(3.4rem, 15vw, 12rem);
  line-height: 1;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: rgba(244, 227, 200, .45);
  /* No text-shadow. At this size a blurred copy of every letter is the single most expensive
     thing on the screen, and at 45% opacity it was not visible anyway. */
  will-change: transform;
  transform: translateX(100vw);
  animation: bootCrawl 34s linear 5s infinite;
}
/* Off the right, across, and away to the left. -100% is the line's own width, so it is fully
   gone before it starts again. */
@keyframes bootCrawl {
  from { transform: translateX(100vw); }
  to { transform: translateX(-100%); }
}

/* Depth order, bottom to top: flares, ticker, sparks, mark. */
.boot-flare { position: absolute; border-radius: 50%; filter: blur(48px); z-index: 0; }
/* THEY USED TO BE THERE FROM THE FIRST FRAME, which meant the screen opened on a wash of
   colour and the logo faded up into something already happening. They arrive with the pulse
   instead: two seconds of nothing but black, the mark, and then the room lighting around it.
   The drift and the fade are separate animations on separate properties, so neither replaces
   the other -- transform for the movement, opacity for the arrival. */
.boot-flare { opacity: 0; }
.boot-flare-a {
  width: 46vmax; height: 46vmax; left: -12vmax; top: -14vmax;
  background: radial-gradient(circle, rgba(214, 92, 96, .40), rgba(214, 92, 96, 0) 68%);
  animation: bootDriftA 19s ease-in-out infinite, bootFlareIn 3.2s ease 2s forwards;
}
.boot-flare-b {
  width: 38vmax; height: 38vmax; right: -10vmax; bottom: -12vmax;
  background: radial-gradient(circle, rgba(181, 126, 232, .34), rgba(181, 126, 232, 0) 68%);
  animation: bootDriftB 26s ease-in-out infinite, bootFlareIn 3.6s ease 2.3s forwards;
}
@keyframes bootFlareIn { to { opacity: 1; } }
@keyframes bootDriftA {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50% { transform: translate(9vmax, 7vmax) scale(1.16); }
}
@keyframes bootDriftB {
  0%, 100% { transform: translate(0, 0) scale(1.1); }
  50% { transform: translate(-8vmax, -6vmax) scale(.94); }
}

@media (prefers-reduced-motion: reduce) {
  /* The mark still arrives and the screen still works; what goes is the drifting and the
     breathing. */
  .boot-logo { animation: bootFade 2s linear forwards; }
  .boot-spark { animation: none; }
  /* The flares still have to arrive, or reduced motion leaves them invisible for good. */
  .boot-flare { animation: bootFlareIn 3.2s ease 2s forwards; }
  .boot-spark { opacity: .7; }
  /* The strip still has to appear, or reduced motion leaves it invisible for good -- the fade
     in is what makes it visible at all. */
  .boot-ticker { animation: bootTickerIn 1.1s ease 5s forwards; }
}

@media (prefers-reduced-motion: reduce) {
  /* A crawl that cannot move is just a line of text. Parked at the left edge and left alone,
     rather than frozen mid-pass with half the phrase off screen. The strip still has to fade in,
     or it stays invisible for good. */
  .boot-ticker { animation: bootTickerIn 1.1s ease 5s forwards; }
  .boot-ticker-line { animation: none; transform: none; padding-left: 4vw; }
}

/* The gate opening. It used to be removed on the same tick as the click, so the shop appeared
   with a hard cut that read as a page load. Now it dissolves and lets the experience chooser
   come up through it.

   pointer-events goes immediately rather than with the fade: for the second and a half it is
   still on screen it is only an image, and a click meant for the chooser behind it should land
   there and not on a gate that has already been answered. */
.gate-backdrop.gate-passed {
  opacity: 0;
  pointer-events: none;
  transition: opacity 1.4s ease;
}
/* A touch of drift as it goes, so it lifts away rather than simply thinning out. */
.gate-backdrop.gate-passed .gate { transform: translateY(-14px) scale(1.02); opacity: 0;
  transition: transform 1.4s cubic-bezier(.22, .61, .36, 1), opacity .9s ease; }
@media (prefers-reduced-motion: reduce) {
  .gate-backdrop.gate-passed, .gate-backdrop.gate-passed .gate { transition: none; }
}
