/* ===========================================================================
   VAULT  —  ui: header, footer, buttons, cards, stats, marquee, accordion
   ---------------------------------------------------------------------------
   Everything here is shared by every page. Each piece reads its colours from
   the tokens, so none of it has a second copy for a second background.
   =========================================================================== */

/* ============================================================== PROGRESS == */

#progress {
  position: fixed; inset: 0 auto auto 0; z-index: 120;
  height: 2px; width: 0;
  background: var(--gold-grad);
  box-shadow: 0 0 12px rgba(224, 168, 60, .6);
  transition: width .12s linear;
}

/* ================================================================ HEADER ==
   Glass, and it CONDENSES on scroll: full height over the hero, tighter and
   more opaque once the page has moved. That single change is most of what
   makes a site feel considered on the way down.
   ========================================================================= */

.bar {
  position: sticky; top: 0; z-index: 100;
  background: rgba(10, 11, 13, .6);
  backdrop-filter: blur(18px) saturate(1.4);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  border-bottom: 1px solid transparent;
  transition: background .3s var(--ease), border-color .3s var(--ease);
}
.bar.scrolled {
  background: rgba(10, 11, 13, .86);
  border-bottom-color: var(--line);
}

.bar-in {
  width: 100%; max-width: var(--col); margin: 0 auto;
  padding: 0 var(--pad);
  display: flex; align-items: center; gap: var(--s3);
  height: 78px;
  transition: height .3s var(--ease);
}
.bar.scrolled .bar-in { height: var(--bar-h); }

.brand {
  display: flex; align-items: center; gap: 10px; flex: none;
  font-weight: 800; font-size: 1.06rem; letter-spacing: -.02em;
  color: var(--tx);
}
.brand img { width: 28px; height: 28px; }
.brand i { font-style: normal; color: var(--red); }

.crumb {
  flex: none; padding-left: var(--s3);
  border-left: 1px solid var(--line-2);
  font-size: .78rem; font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--gold);
}
@media (max-width: 900px) { .crumb { display: none; } }

.nav { margin-left: auto; }
.nav-in { display: flex; align-items: center; gap: 2px; }
.nav a {
  position: relative;
  display: flex; align-items: center; gap: 6px;
  padding: 9px 14px; border-radius: var(--r1);
  font-size: .88rem; font-weight: 500; color: var(--tx-2);
  transition: color .18s var(--ease), background .18s var(--ease);
}
.nav a:hover { color: var(--tx); background: rgba(255, 255, 255, .05); }
.nav a[aria-current] { color: var(--tx); }
.nav a[aria-current]::after {
  content: ''; position: absolute; left: 14px; right: 14px; bottom: 2px;
  height: 2px; border-radius: 2px; background: var(--gold);
}
.nav .ext .i { width: 12px; height: 12px; opacity: .5; }

.bar .btn-sm { flex: none; margin-left: var(--s2); }

/* ---- the burger, and the drawer it opens ---------------------------------
   Below 940px the links move into a panel that slides down out of the bar.
   The previous header simply hid them, which on a shop is a dead end. */

.burger {
  display: none; flex: none;
  width: 42px; height: 42px; border-radius: var(--r1);
  border: 1px solid var(--line-2);
  position: relative;
}
.burger span {
  position: absolute; left: 12px; right: 12px; height: 2px;
  border-radius: 2px; background: var(--tx);
  transition: transform .3s var(--ease), opacity .2s var(--ease);
}
.burger span:first-child { top: 16px; }
.burger span:last-child { bottom: 16px; }
.burger[aria-expanded="true"] span:first-child { transform: translateY(6px) rotate(45deg); }
.burger[aria-expanded="true"] span:last-child { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 940px) {
  .burger { display: block; }
  .bar .btn-sm { margin-left: auto; }
  .bar-in { height: 64px; }
  .bar.scrolled .bar-in { height: 58px; }

  .nav {
    position: absolute; left: 0; right: 0; top: 100%; margin: 0;
    background: rgba(10, 11, 13, .97);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
    /* 0fr -> 1fr transitions to a height nobody has to measure. It collapses
       ONE track, which is why every link lives in .nav-in. */
    display: grid; grid-template-rows: 0fr;
    transition: grid-template-rows .34s var(--ease);
  }
  .nav.open { grid-template-rows: 1fr; }
  .nav-in {
    min-height: 0; overflow: hidden;
    flex-direction: column; align-items: stretch; gap: 0;
    padding: 0 var(--pad);
  }
  .nav.open .nav-in { border-bottom: 1px solid var(--line); }
  .nav a {
    padding: 15px 2px; border-radius: 0; font-size: 1rem;
    border-bottom: 1px solid var(--line);
  }
  .nav a:last-child { border-bottom: 0; margin-bottom: var(--s2); }
  .nav a:hover { background: none; }
  .nav a[aria-current]::after { left: 2px; right: auto; width: 18px; bottom: 9px; }
}
@media (max-width: 420px) {
  .bar .btn-sm span { display: none; }
}

/* =============================================================== BUTTONS == */

.cta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); }
.cta.mid { justify-content: center; }

.btn {
  position: relative; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  padding: 15px 26px; border-radius: var(--r2);
  font-size: .92rem; font-weight: 650; letter-spacing: -.01em;
  white-space: nowrap;
  transition: background .22s var(--ease), color .22s var(--ease),
              border-color .22s var(--ease), box-shadow .28s var(--ease),
              transform .12s var(--ease);
}
.btn:active { transform: translateY(1px); }
.btn .i { width: 17px; height: 17px; }

/* THE ONE PLACE THE ACCENT IS A FILL. Everywhere else gold is a hairline or
   a word; on the thing somebody is meant to press it is the whole button.
   The sheen is a single pass on hover, not a loop — a button that shimmers
   by itself is a button nobody believes. */
.btn-gold {
  background: var(--gold-grad); color: #161104;
  box-shadow: var(--sh-gold);
}
.btn-gold::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(105deg, transparent 32%, rgba(255,255,255,.55) 50%, transparent 68%);
  transform: translateX(-130%);
  transition: transform .7s var(--ease);
}
.btn-gold:hover { box-shadow: 0 16px 44px -12px rgba(224, 168, 60, .68); }
.btn-gold:hover::after { transform: translateX(130%); }

.btn-line {
  color: var(--tx);
  border: 1px solid var(--line-2);
  background: rgba(255, 255, 255, .02);
}
.btn-line:hover { border-color: rgba(255,255,255,.3); background: rgba(255, 255, 255, .06); }

.btn-ghost { color: var(--tx-2); }
.btn-ghost:hover { color: var(--tx); background: rgba(255, 255, 255, .05); }

.btn-sm { padding: 10px 18px; font-size: .84rem; border-radius: var(--r1); }
.btn-sm .i { width: 15px; height: 15px; }
.btn-lg { padding: 18px 34px; font-size: 1rem; }

/* A link that reads as the next step without pretending to be a button. */
.go {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: .9rem; font-weight: 650; color: var(--gold);
}
.go .i { width: 15px; height: 15px; transition: transform .25s var(--ease); }
.go:hover .i { transform: translateX(4px); }

/* ================================================================= PILLS == */

.pill {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 13px; border-radius: var(--rp);
  border: 1px solid var(--line-2);
  background: rgba(255, 255, 255, .03);
  font-size: .76rem; font-weight: 600; letter-spacing: .01em; color: var(--tx-2);
}
.pill .i { width: 13px; height: 13px; color: var(--gold); }
.pill.gold { border-color: rgba(224,168,60,.34); background: var(--gold-wash); color: var(--gold-lit); }
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); }

.pills { display: flex; flex-wrap: wrap; gap: 8px; }

/* ================================================================= CARDS ==
   One card, used everywhere. The spotlight is a radial highlight that follows
   the cursor across it — site.js writes --mx/--my, CSS does the rest, and
   with scripting off it simply never appears.
   ========================================================================= */

.card {
  position: relative; isolation: isolate;
  border-radius: var(--r3);
  border: 1px solid var(--line);
  background: var(--surf);
  transition: border-color .3s var(--ease), background .3s var(--ease),
              transform .35s var(--ease), box-shadow .35s var(--ease);
}
.card::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit; opacity: 0;
  background: radial-gradient(320px circle at var(--mx, 50%) var(--my, 50%),
              rgba(224, 168, 60, .1), transparent 62%);
  transition: opacity .35s var(--ease);
}
.card:hover { border-color: var(--line-2); background: var(--surf-2); }
.card:hover::before { opacity: 1; }
.card.lifts:hover { transform: translateY(-4px); box-shadow: var(--sh-2); }

/* ---- the feature tile ---- */

.tiles { display: grid; gap: var(--s3); grid-template-columns: repeat(4, 1fr); }
.tiles.three { grid-template-columns: repeat(3, 1fr); }
.tiles.two   { grid-template-columns: repeat(2, 1fr); }
@media (max-width: 1000px) { .tiles, .tiles.three { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px)  { .tiles, .tiles.three, .tiles.two { grid-template-columns: 1fr; } }

.tile { padding: var(--s4); }
.tile .ico {
  display: grid; place-items: center;
  width: 40px; height: 40px; border-radius: var(--r1);
  background: var(--gold-wash); color: var(--gold);
  transition: background .3s var(--ease), color .3s var(--ease);
}
.tile .ico .i { width: 21px; height: 21px; }
.tile:hover .ico { background: var(--gold); color: #161104; }
.tile h3 { margin: var(--s3) 0 8px; }
.tile p { font-size: .94rem; line-height: 1.55; color: var(--tx-3); }

/* ---- the numbered step ---- */

.steps { display: grid; gap: var(--s3); grid-template-columns: repeat(2, 1fr); }
@media (max-width: 820px) { .steps { grid-template-columns: 1fr; } }
.step { padding: var(--s5); }
.step .n {
  font-family: var(--mono); font-size: .82rem; font-weight: 600;
  letter-spacing: .1em; color: var(--gold); opacity: .8;
}
.step h3 { margin: var(--s3) 0 var(--s2); font-size: 1.24rem; }
.step p { font-size: .98rem; color: var(--tx-2); }

/* ================================================================= STATS ==
   The numbers count up the first time they are seen. Static digits read as
   decoration; a number that arrives reads as a fact.
   ========================================================================= */

.stats {
  display: grid; grid-template-columns: repeat(4, 1fr);
  border-radius: var(--r3); border: 1px solid var(--line);
  background: var(--surf); overflow: hidden;
}
@media (max-width: 760px) { .stats { grid-template-columns: repeat(2, 1fr); } }
.stat { padding: var(--s5) var(--s4); text-align: center; position: relative; }
.stat + .stat::before {
  content: ''; position: absolute; left: 0; top: 18%; bottom: 18%;
  width: 1px; background: var(--line);
}
@media (max-width: 760px) { .stat:nth-child(odd)::before { display: none; } }
.stat b {
  display: block;
  font-size: clamp(2.2rem, 4.4vw, 3rem); font-weight: 800;
  letter-spacing: -.04em; line-height: 1;
  background: var(--gold-grad);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  font-variant-numeric: tabular-nums;
}
.stat span {
  display: block; margin-top: var(--s2);
  font-size: .82rem; font-weight: 500; color: var(--tx-3);
}

/* =============================================================== MARQUEE ==
   The frameworks it runs on, moving slowly. Duplicated in the markup and
   translated by exactly half, which is the only version that never shows a
   gap at the loop point.
   ========================================================================= */

.marquee {
  position: relative; overflow: hidden; padding: var(--s5) 0;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
/* THE LOOP POINT HAS TO LAND EXACTLY, so each copy of the list is its own
   flex child and the gap lives INSIDE it.

   It used to be sixteen items in one flex row with a gap between all of them,
   animated to translateX(-50%). That does not line up: sixteen items carry
   fifteen gaps, so half the track is one set plus seven-and-a-half gaps, while
   the second set actually begins one set plus eight gaps along. The track
   jumped half a gap every lap and tore a hole in the strip.

   Two sets, each carrying its own trailing gap, are exactly 50% each. */
.marquee-track {
  display: flex; width: max-content;
  animation: slide 38s linear infinite;
}
.marquee-set { display: flex; gap: var(--s6); padding-right: var(--s6); }
.marquee:hover .marquee-track { animation-play-state: paused; }
@keyframes slide { to { transform: translateX(-50%); } }
.marquee-item {
  display: flex; align-items: center; gap: 11px;
  font-size: .95rem; font-weight: 600; color: var(--tx-3);
  white-space: nowrap;
}
.marquee-item .i { width: 18px; height: 18px; color: var(--gold-dim); }

/* =============================================================== ACCORDION ==
   The FAQ. Objections answered on the page are objections that do not become
   a message nobody sends. Height is animated with grid-template-rows so no
   pixel value has to be measured or maintained.
   ========================================================================= */

/* THE ROW IS A GRID, NOT A FLEX WITH AN AUTO MARGIN.

   It was `display:flex` with `margin-left:auto` on the caret, which pushes the
   icon to the far edge of the BUTTON - and the button carries 26px of padding
   all round, so the caret ended up floating in a pocket of dead space that did
   not match the gap on the left. Two columns, question and caret, put the icon
   on a real edge and the text is free to wrap without shoving it. */
.faq { display: grid; gap: 10px; max-width: 820px; margin: 0 auto; }
.qa {
  border-radius: var(--r2);
  border: 1px solid var(--line);
  background: var(--surf);
  transition: border-color .22s var(--ease), background .22s var(--ease);
}
.qa:hover { border-color: var(--line-2); background: var(--surf-2); }
.qa.open { border-color: rgba(224, 168, 60, .3); background: var(--surf-2); }

.qa > button {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: var(--s4);
  width: 100%; padding: 19px var(--s4);
  text-align: left; font-size: 1rem; font-weight: 600;
  letter-spacing: -.01em; color: var(--tx-2);
  transition: color .18s var(--ease);
}
.qa:hover > button, .qa.open > button { color: var(--tx); }

/* The caret sits in its own round well rather than floating as a bare glyph.
   It reads as a control that way, which is what stops the row looking like a
   line of text with a stray arrow after it. */
.qa > button .i {
  width: 30px; height: 30px; padding: 7px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .05);
  color: var(--tx-3);
  transition: transform .3s var(--ease), color .22s var(--ease),
              background .22s var(--ease);
}
.qa:hover > button .i { color: var(--tx-2); }
.qa.open > button .i {
  transform: rotate(180deg);
  background: var(--gold-wash); color: var(--gold);
}
.qa-body {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows .34s var(--ease);
}
.qa.open .qa-body { grid-template-rows: 1fr; }
.qa-body > div { overflow: hidden; }
/* The answer keeps the question's left edge and clears the caret on the right,
   so the two lines up rather than the text running under the control. */
.qa-body p {
  padding: 2px var(--s6) 20px var(--s4);
  font-size: .96rem; line-height: 1.62; color: var(--tx-3); max-width: 68ch;
}
.qa-body p + p { padding-top: var(--s2); }
.qa-body a { color: var(--gold); text-decoration: underline; text-underline-offset: 3px; }
.qa-body code {
  font-family: var(--mono); font-size: .88em; color: var(--gold-lit);
  background: rgba(255,255,255,.05); padding: 2px 6px; border-radius: 5px;
}

/* ---- a checked list, used wherever a card has to enumerate ---- */

.ticks { display: grid; gap: 9px; }
.lead + .ticks { margin-top: var(--s4); }

/* THE ROW IS NOT A FLEX CONTAINER, and it cannot become one again.

   It was `display:flex` with the tick and the text as two items, which is
   right only while the text is a single unbroken run. The moment a line
   carries inline markup - "118 strings in <code>locales/en.lua</code>, ready
   to translate" - flex makes EVERY run its own item: text, code, text, laid
   out in a row that cannot wrap as a sentence. On a phone that row refused to
   shrink, held the whole card 22px wider than the column, and put the page
   into horizontal scroll; forcing the items to shrink instead made the code
   overlap the words after it.

   Absolutely positioning the tick leaves the text as ordinary inline flow, so
   it wraps like prose and the row's min-content is just its longest word. */
.ticks li {
  display: block; position: relative;
  padding-left: 27px;
  font-size: .93rem; color: var(--tx-2);
}
.ticks .i {
  position: absolute; left: 0; top: 3px;
  width: 17px; height: 17px; color: var(--gold);
}

/* ================================================================ CLOSER == */

.closer { text-align: center; }
/* THE BUTTONS TOO, not just the words. .cta is a flex row, and text-align does
   nothing to flex children - so a centred closing section had its buttons
   sitting hard against the left margin. There is a .cta.mid modifier for this,
   but a modifier you have to remember is one somebody will forget: inside a
   .closer the buttons are always centred. */
.closer .cta { justify-content: center; }
.closer h2 { max-width: 24ch; margin: var(--s3) auto var(--s3); }
.closer .lead { margin: 0 auto var(--s5); }

/* ================================================================ FOOTER == */

.foot { position: relative; border-top: 1px solid var(--line); background: var(--bg-sink); padding: var(--s7) 0 0; }
.foot-grid {
  display: grid; grid-template-columns: 1.7fr 1fr 1fr 1fr; gap: var(--s5);
  padding-bottom: var(--s6);
}
@media (max-width: 880px) { .foot-grid { grid-template-columns: 1fr 1fr; row-gap: var(--s6); } }
@media (max-width: 520px) { .foot-grid { grid-template-columns: 1fr; } }

.foot-brand p { margin: var(--s3) 0 var(--s4); max-width: 38ch; font-size: .94rem; color: var(--tx-3); }
.foot-col h2 {
  font-size: .76rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  color: var(--tx-4); margin-bottom: var(--s3);
}
.foot-col a {
  display: block; padding: 6px 0; font-size: .94rem; color: var(--tx-2);
  transition: color .16s var(--ease), transform .16s var(--ease);
}
.foot-col a:hover { color: var(--gold); transform: translateX(3px); }

/* ============================================================== PAYMENTS ==
   The strip above the copyright line. It is the last thing on every page and
   the first thing somebody scrolls to when they are deciding whether a shop
   they have never heard of is real, so it is a row of marks and one plain
   paragraph rather than a wall of badges. */
.paystrip {
  padding: var(--s4) 0;
  border-top: 1px solid var(--line);
  display: grid; gap: var(--s2);
}

.paystrip-row {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s2) var(--s3);
}

.paystrip-k {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: .86rem; color: var(--tx-3);
}
.paystrip-k b { color: var(--tx-2); font-weight: 700; }
.paystrip-k .i { width: 16px; height: 16px; fill: var(--gold); flex: none; }

.paylogos {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 8px;
}
.paylogos li {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 12px; border-radius: var(--r1);
  border: 1px solid var(--line);
  background: rgba(255,255,255,.03);
  font-size: .76rem; font-weight: 600; letter-spacing: .01em;
  color: var(--tx-3);
}
/* The brand marks are single-path and monochrome, so they take the row's
   colour rather than arriving in four different brand palettes and turning
   the footer into a sticker album. */
.paylogos .i { width: 19px; height: 19px; fill: var(--tx-2); flex: none; }

.paystrip-note {
  margin: 0; max-width: 78ch;
  font-size: .8rem; line-height: 1.6; color: var(--tx-4);
}
.paystrip-note b { color: var(--tx-3); font-weight: 700; }
.paystrip-note a { color: var(--tx-3); text-decoration: underline; text-underline-offset: 3px; }
.paystrip-note a:hover { color: var(--gold); }
.paystrip-note code {
  font-family: var(--mono); font-size: .92em;
  padding: .1em .35em; border-radius: 5px;
  background: rgba(255,255,255,.05); color: var(--tx-3);
}

@media (max-width: 620px) {
  .paystrip-row { gap: var(--s2); }
  .paylogos li { padding: 6px 10px; font-size: .72rem; }
}

.foot-bottom {
  display: flex; flex-wrap: wrap; gap: var(--s3);
  justify-content: space-between;
  padding: var(--s4) 0;
  border-top: 1px solid var(--line);
  font-size: .84rem; color: var(--tx-4);
}


/* ======================================================== the sale strip ===
   Revealed by js/shop.js only when Tebex is really applying a discount and the
   deadline has not passed. It carries no number of its own - see the markup in
   _shell.html and salePercent() in shop.js. */
.salebar {
  position: relative; z-index: 60;
  background: linear-gradient(90deg, #7d1c18, #b8322a 45%, #7d1c18);
  color: #fff5ee;
  border-bottom: 1px solid rgba(0,0,0,.4);
}
.salebar[hidden] { display: none; }
.sb-in {
  max-width: var(--col, 1200px); margin: 0 auto;
  padding: 9px var(--pad, 24px);
  display: flex; align-items: center; gap: 14px;
  font-size: 14px; font-weight: 600;
  flex-wrap: wrap; justify-content: center;
}
.sb-pct {
  font-weight: 900; font-size: 17px; letter-spacing: .02em;
  background: rgba(0,0,0,.28); border-radius: 999px; padding: 2px 12px;
}
.sb-txt b { font-weight: 800; }
.sb-until { opacity: .82; font-variant-numeric: tabular-nums; }
.sb-cta {
  margin-left: 4px; color: #fff; text-decoration: none; font-weight: 800;
  border-bottom: 1px solid rgba(255,255,255,.55);
}
.sb-cta:hover { border-bottom-color: #fff; }
@media (max-width: 640px) { .sb-until { width: 100%; text-align: center; } }

/* The percentage badge inside the shop card's promo block. */
.promo-pct {
  margin-left: auto;
  font-weight: 900; font-size: 15px;
  color: var(--danger-glow, #e8564f);
}
.promo-pct[hidden] { display: none; }

/* ====================================================== the buy modal ======
   A short read, a look at it, and the price. The gallery is a row that scrolls
   sideways rather than a grid that grows downwards, because the one thing a
   dialog must not do is become taller than the screen it opened on. */
.shop-modal-lede {
  margin: 0 0 16px;
  color: var(--tx-2, #a7aeb9);
  font-size: 15px; line-height: 1.6;
}
/* A LEAD IMAGE WITH TWO UNDER IT. What this replaces was grid-auto-flow:
   column at 62% wide - a filmstrip. The third and fourth images sat off the
   edge, and a dialog is the one place a sideways scrollbar never gets found.
   Three pictures in a shape people already know how to read, all visible at
   once, and no taller than the dialog. */
.shop-gal {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  /* It sits BELOW the button and the fine print now, so the space it needs is
     above it - enough to read as a separate thing rather than as more of the
     small print. */
  margin: 22px 0 0;
}
.shop-gal .sg-lead { grid-column: 1 / -1; }
.shop-gal a {
  display: block; border-radius: 10px; overflow: hidden;
  border: 1px solid var(--line, rgba(255,255,255,.09));
  background: var(--bg-sink, #0e1013);
  transition: border-color .18s ease, transform .18s ease;
}
.shop-gal a:hover { border-color: var(--gold, #e0a83c); transform: translateY(-1px); }
.shop-gal img { width: 100%; display: block; aspect-ratio: 16 / 9; object-fit: cover; }
/* THE LEAD IS NOT CROPPED. It is a 21:9 grab and the things worth seeing in it
   - the pot, the chamber counter, both player panels - all live in the four
   corners, which is exactly what `cover` in a 16:9 box throws away. It gets
   its own shape; the two underneath are only a glimpse and can crop. */
.shop-gal .sg-lead img { aspect-ratio: auto; height: auto; }

/* ======================================================== the hero clip ====
   Sits over the drawn hand and only earns the space once it is really playing.
   See heroClip() in site.js for why the fallback is left underneath. */
.hero-art .hero-clip {
  position: absolute; inset: 0; z-index: 1;
  width: 100%; height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity .5s ease;
  pointer-events: none;
}
.hero-art.clip-live .hero-clip { opacity: 1; }
.hero-art.clip-live .fan { opacity: 0; transition: opacity .4s ease; }



/* ==========================================================================
   MOVED HERE FROM product.css.
   The shop card and the buy modal are emitted on the product page AND on
   the home page, so their styles cannot live in a stylesheet only one of
   them loads - the home page's modal came out as an unstyled column with a
   900px logo in it. The markup was already a partial; this is the other
   half of that decision.
   ========================================================================== */

/* ---- the extract ---- */

.cfgcode {
  margin-top: var(--s3);
  border: 1px solid var(--line); border-radius: var(--r3);
  background: var(--bg-sink); overflow: hidden;
}
.cfgcode-bar {
  display: flex; align-items: center; gap: var(--s3);
  padding: 12px var(--s4);
  border-bottom: 1px solid var(--line);
  background: rgba(255, 255, 255, .015);
  font-size: .8rem; color: var(--tx-3);
}
.cfgcode-note { margin-left: auto; color: var(--tx-4); }
.cfgcode pre { padding: var(--s4); overflow-x: auto; }
.cfgcode code {
  font-family: var(--mono); font-size: .84rem; line-height: 1.75;
  color: var(--tx-2); white-space: pre;
}
/* Three colours and no more. A config extract wants to read as prose with
   values in it, not as a syntax-highlighting demo. */
.cfgcode .c { color: var(--tx-4); }
.cfgcode .n { color: var(--gold-lit); }
.cfgcode .s { color: var(--green); }
.cfgcode .k { color: var(--red-lit); }

/* An inline code span in body copy, distinct from the block above. */
code.inline {
  font-family: var(--mono); font-size: .88em; color: var(--gold-lit);
  background: rgba(255, 255, 255, .05); padding: 2px 6px; border-radius: 5px;
}

/* ============================================================ the storefront ==
   OUR checkout, on our domain, against Tebex's Headless API. Tebex still takes
   the payment - card details on a page we control would put this domain in PCI
   scope - but the browsing, the price, the sign-in and the cart are ours.

   THE PRICE IS NOT IN THE MARKUP. It is read from Tebex on load, because a
   number typed into a page goes stale in silence the first time it changes and
   structured data that disagrees with the checkout is a policy problem, not a
   cosmetic one.
-------------------------------------------------------------------------- */

.shop {
  display: grid; grid-template-columns: 1.4fr .6fr;
  gap: clamp(18px, 2.6vw, 30px);
  align-items: start;
  max-width: 940px; margin: clamp(28px, 4vw, 44px) auto 0;
  text-align: left;
}
@media (max-width: 780px) { .shop { grid-template-columns: 1fr; } }

.shop-card {
  padding: clamp(24px, 3vw, 34px);
  border-radius: var(--r-lg, 18px);
  border: 1px solid rgba(224,168,60,.3);
  background:
    radial-gradient(120% 150% at 10% 0%, rgba(224,168,60,.14), transparent 60%),
    linear-gradient(180deg, var(--surf), var(--bg-lift));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.06), 0 26px 60px rgba(0,0,0,.5);
}

.shop-head { display: flex; align-items: center; gap: 16px; margin-bottom: 22px; }
.shop-head img { width: 92px; height: auto; flex: none; }
.shop-head b { display: block; font-size: 19px; font-weight: 800; color: var(--tx); }
.shop-head span { display: block; margin-top: 4px; font-size: 13.5px; color: var(--tx-3); }

.shop-price { margin-bottom: 20px; }
.shop-amount {
  display: block;
  font-size: clamp(38px, 5vw, 52px); font-weight: 800; line-height: 1;
  letter-spacing: -.03em;
  background: var(--gold-grad); -webkit-background-clip: text; color: transparent;
  font-variant-numeric: tabular-nums;
}
/* A shimmer while Tebex answers, so the space does not jump when it lands. */
.shop-amount.is-loading {
  min-width: 5ch; border-radius: 6px;
  background: linear-gradient(90deg, var(--surf-2), var(--line-2), var(--surf-2));
  background-size: 200% 100%;
  animation: shopwait 1.2s linear infinite;
  -webkit-background-clip: initial; color: transparent;
}
@keyframes shopwait { to { background-position: -200% 0; } }
.shop-note { display: block; margin-top: 9px; font-size: 13.5px; color: var(--tx-3); }
.shop-note s { color: var(--tx-4); }

.shop-ticks { list-style: none; margin: 0 0 22px; padding: 0; display: grid; gap: 9px; }
.shop-ticks li {
  display: flex; align-items: center; gap: 10px;
  font-size: 14.5px; color: var(--tx-2);
}
.shop-ticks .i { width: 17px; height: 17px; flex: none; fill: var(--green); }

.shop-buy { width: 100%; justify-content: center; }
.shop-buy[disabled] { opacity: .55; cursor: not-allowed; }
/* BUSY MEANS SOMETHING IS HAPPENING, AND IT HAS TO LOOK LIKE IT.
   The button now does two round trips to Tebex before the page changes - a
   basket and a sign-in link - so there is a real moment where a customer has
   clicked Buy and nothing has visibly happened. Half a second of that is how
   you get a second click, or a leave. It dims, it stops taking clicks, and it
   says so. */
.shop-buy.is-busy {
  pointer-events: none;
  opacity: .72;
  position: relative;
}
.shop-buy.is-busy .btn-t { visibility: hidden; }
.shop-buy.is-busy::after {
  content: 'Taking you to the checkout…';
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-weight: 700;
}

.shop-state { margin: 12px 0 0; font-size: 13.5px; color: var(--tx-2); min-height: 1em; }
.shop-state.bad { color: var(--red-lit); }
.shop-state.warn { color: var(--gold); }
.shop-state:empty { min-height: 0; }

.shop-fine { margin: 14px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--tx-4); }

.shop-aside { display: grid; gap: 10px; }
.shop-aside .btn { width: 100%; justify-content: flex-start; }

/* ---------------------------------------------------------------- promo ----
   The launch code. `hidden` in the markup and revealed by js/shop.js only once
   a real price has come back from Tebex - a 30%-off badge sitting next to a
   price that failed to load is worse than no badge at all.
---------------------------------------------------------------------------- */

/* ================================================================ THE SALE ==
   ONE ROW. What this replaces was a dashed box wrapping a header row, wrapping
   a bordered plate that read ON SALE in spaced monospace, wrapping a line of
   fine print - three nested frames around two facts, and the dashes made the
   whole thing read like a placeholder somebody forgot to style.

   A sale has two things to say: how much off, and by when. So the figure is a
   badge big enough to BE the thing you see, the deadline sits beside it, and
   the rest is one line of small type. */
.promo {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 0 0 22px;
  padding: 13px 15px;
  border-radius: 14px;
  border: 1px solid rgba(224,168,60,.34);
  background:
    radial-gradient(90% 200% at 0% 50%, rgba(224,168,60,.16), transparent 70%),
    rgba(224,168,60,.05);
}
.promo[hidden] { display: none; }

/* The figure. Dark on gold rather than gold on dark: it is the one element in
   the card that should read before anything around it, and inverting is how
   you get that without making it bigger than the price. */
.promo-pct {
  flex: none;
  display: grid; place-items: center;
  min-width: 64px; padding: 9px 11px;
  border-radius: 11px;
  background: linear-gradient(160deg, #f0be55, var(--gold));
  color: #17120a;
  font-size: 19px; font-weight: 800; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 4px 14px rgba(224,168,60,.28);
}
.promo-pct[hidden] { display: none; }

.promo-body { min-width: 0; }

.promo-label {
  display: block;
  font-size: 11px; font-weight: 700; letter-spacing: .13em;
  text-transform: uppercase; color: var(--gold);
}

.promo-line {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 10px;
  margin: 3px 0 0;
  font-size: 14px; color: var(--tx);
}
.promo-off { font-weight: 700; }
/* Empty when Tebex reports no discount, and an empty span should not leave a
   gap where a saving would have been. */
.promo-off:empty { display: none; }

.promo-left { display: inline-flex; align-items: center; gap: 5px; color: var(--tx-3); }
.promo-left .i { width: 13px; height: 13px; flex: none; fill: var(--tx-4); }

.promo-total { margin: 2px 0 0; font-size: 12px; color: var(--tx-4); }
.promo-total b { color: var(--tx-3); }

/* Narrow: the badge goes above rather than squeezing the sentence to two
   words a line. */
@media (max-width: 420px) {
  .promo { flex-direction: column; align-items: flex-start; gap: 10px; }
}

.shop-ticks { list-style: none; margin: 0 0 22px; padding: 0; display: grid; gap: 9px; }
.shop-ticks li {
  display: flex; align-items: center; gap: 10px;
  font-size: 14.5px; color: var(--tx-2);
}
.shop-ticks .i { width: 17px; height: 17px; flex: none; fill: var(--green); }

.shop-buy { width: 100%; justify-content: center; }
.shop-buy[disabled] { opacity: .55; cursor: not-allowed; }


.shop-state { margin: 12px 0 0; font-size: 13.5px; color: var(--tx-2); min-height: 1em; }
.shop-state.bad { color: var(--red-lit); }
.shop-state.warn { color: var(--gold); }
.shop-state:empty { min-height: 0; }

.shop-fine { margin: 14px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--tx-4); }

.shop-aside { display: grid; gap: 10px; }
.shop-aside .btn { width: 100%; justify-content: flex-start; }

/* ---------------------------------------------------------------- promo ----
   The launch code. `hidden` in the markup and revealed by js/shop.js only once
   a real price has come back from Tebex - a 30%-off badge sitting next to a
   price that failed to load is worse than no badge at all.
---------------------------------------------------------------------------- */

.promo {
  margin: 0 0 22px; padding: 14px 16px 13px;
  border-radius: 14px;
  border: 1px dashed rgba(224,168,60,.42);
  background: linear-gradient(180deg, rgba(224,168,60,.10), rgba(224,168,60,.03));
}
.promo[hidden] { display: none; }

.promo-top { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.promo-top .i { width: 17px; height: 17px; flex: none; fill: var(--gold); }
.promo-top b {
  font-size: 12px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--gold);
}

/* It was a button that copied a coupon code. The sale needs no code, so this
   is a plate that states it - `.is-static` drops the pointer and the hover. */
.promo-code {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px 12px; border-radius: 10px; cursor: pointer;
  border: 1px solid rgba(224,168,60,.45);
  background: rgba(0,0,0,.30);
  color: var(--tx); text-align: left;
  transition: border-color .16s, background .16s;
}
.promo-code:hover { border-color: var(--gold); background: rgba(0,0,0,.45); }
.promo-code.is-static { cursor: default; }
.promo-code.is-static:hover {
  border-color: rgba(224,168,60,.45); background: rgba(0,0,0,.30);
}
/* Empty when Tebex is not actually applying a discount, and an empty span
   should not leave a gap where a saving would have been. */
.promo-off:empty { display: none; }
.promo-key {
  font-family: var(--mono); font-size: 17px; font-weight: 700;
  letter-spacing: .18em; color: var(--gold);
}
.promo-off {
  margin-left: auto; font-size: 13px; font-weight: 700; color: var(--tx-2);
}
.promo-code .i { width: 15px; height: 15px; flex: none; fill: var(--tx-4); }
.promo-code.copied .i { fill: var(--green); }

.promo-fine {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px;
  margin: 10px 0 0; font-size: 12.5px; color: var(--tx-3);
}
.promo-total b { color: var(--tx); }
.promo-left { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; }
.promo-left .i { width: 14px; height: 14px; fill: var(--tx-4); }

/* ---------------------------------------------------------------- modal ----
   <dialog> so focus trapping, Esc and the inert background are the browser's
   job rather than ours. The card inside is the same partial as the panel at
   the bottom of the page; only the frame around it differs.
---------------------------------------------------------------------------- */

.shop-modal {
  width: min(560px, calc(100vw - 32px));
  max-height: calc(100dvh - 32px);
  padding: 0; border: 0; background: none; overflow: visible;
  margin: auto;                       /* centred in both axes, not just across */
}
/* showModal() makes the page inert but does not stop it scrolling behind the
   dialog, which on a long product page is disorienting. */
body:has(.shop-modal[open]) { overflow: hidden; }

.shop-modal::backdrop {
  background: rgba(6,6,8,.78);
  backdrop-filter: blur(6px);
}
.shop-modal[open] { animation: shoppop .22s cubic-bezier(.2,.8,.3,1); }
@keyframes shoppop {
  from { opacity: 0; transform: translateY(14px) scale(.985); }
}
@media (prefers-reduced-motion: reduce) { .shop-modal[open] { animation: none; } }

.shop-modal-inner {
  position: relative;
  max-height: calc(100dvh - 32px);
  overflow-y: auto;                   /* a short viewport scrolls the card */
  -webkit-overflow-scrolling: touch;
}

.shop-modal-title {
  margin: 0 0 20px; font-size: clamp(21px, 3vw, 26px); line-height: 1.2;
  letter-spacing: -.02em; color: var(--tx); padding-right: 40px;
}

.shop-x {
  position: absolute; top: 14px; right: 14px;
  width: 34px; height: 34px; display: grid; place-items: center;
  border-radius: 9px; cursor: pointer;
  border: 1px solid var(--line); background: rgba(0,0,0,.35);
  transition: border-color .16s, background .16s;
}
.shop-x:hover { border-color: var(--line-2); background: rgba(0,0,0,.6); }
.shop-x .i { width: 16px; height: 16px; fill: var(--tx-3); }

.shop-modal-links {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 20px;
  margin-top: 18px; font-size: 13px;
}
.shop-modal-links a { color: var(--tx-3); text-decoration: none; }
.shop-modal-links a:hover { color: var(--gold); text-decoration: underline; }

/* The modal repeats the panel's logo header, which is redundant when the
   title above already names the product. */
.shop-modal-inner .shop-head img { width: 68px; }
.shop-modal-inner .shop-fine { text-align: center; }

/* The arrow on the hero's buy button. It nudges on hover rather than sitting
   there, which is the difference between a label and an invitation. */
.btn .btn-go {
  margin-left: 2px;
  transition: transform .18s ease;
}
.btn:hover .btn-go { transform: translateX(3px); }
@media (prefers-reduced-motion: reduce) { .btn:hover .btn-go { transform: none; } }

