/* ==========================================================================
   Prospira Tweaks — design system "royal liquid glass"
   Page-agnostic layer: tokens, fonts, primitives, header/nav/footer,
   injected UI (auth.js cart + user menu),
   Tailwind-utility compat for legacy inline scripts.
   Palette and glass recipes mirror the Prospira Tweaking Utility app 1:1.
   ========================================================================== */

/* ---------- Fonts (self-hosted, variable) ---------- */
@font-face {
  font-family: 'Geist Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('/fonts/geist-latin-wght-normal.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Geist Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('/fonts/geist-latin-ext-wght-normal.woff2') format('woff2-variations');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Geist Mono Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('/fonts/geist-mono-latin-wght-normal.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Geist Mono Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('/fonts/geist-mono-latin-ext-wght-normal.woff2') format('woff2-variations');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Instrument Serif';
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url('/fonts/instrument-serif-latin-400-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Instrument Serif';
  font-style: italic;
  font-display: swap;
  font-weight: 400;
  src: url('/fonts/instrument-serif-latin-400-italic.woff2') format('woff2');
}

/* ---------- Design tokens ---------- */
:root {
  /* accent - royal blue, the ONLY accent */
  --accent: #2a5bff;
  --accent-rgb: 42, 91, 255;
  --accent-2: #6366f1;
  --accent-deep: #1e3aae;

  /* surfaces */
  --bg-0: #02030a;
  --bg-1: #04060c;
  --bg-2: #0a0d18;

  /* text */
  --text-hi: #f0f5ff;
  --text-mid: rgba(214, 225, 245, 0.72);
  --text-low: rgba(214, 225, 245, 0.48);

  /* borders */
  --border-soft: rgba(150, 180, 255, 0.08);
  --border-mid: rgba(150, 180, 255, 0.14);
  --border-strong: rgba(150, 180, 255, 0.2);
  --border-accent: rgba(42, 91, 255, 0.42);
  --island-border: rgba(150, 180, 255, 0.07);

  /* liquid glass (verbatim app tokens) */
  --glass-bg:
    radial-gradient(60% 100% at 6% 0%, rgba(190, 210, 255, 0.1), transparent 55%),
    radial-gradient(130% 180% at 0% 0%, rgba(42, 91, 255, 0.09), transparent 56%),
    radial-gradient(80% 120% at 100% 100%, rgba(30, 58, 174, 0.05), transparent 68%),
    linear-gradient(180deg, rgba(11, 15, 30, 0.22), rgba(7, 10, 22, 0.32));
  --glass-blur: blur(26px) saturate(180%);
  --glass-border: rgba(255, 255, 255, 0.05);
  --glass-hairline: linear-gradient(90deg, transparent, rgba(190, 210, 255, 0.4), transparent);
  --glass-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    inset 0 -1px 0 rgba(0, 0, 0, 0.16),
    0 20px 44px -24px rgba(0, 0, 0, 0.6),
    0 0 50px -36px rgba(42, 91, 255, 0.16);

  /* royal card - gradient-only recipe, safe without backdrop blur */
  /* card tokens aliased to the glass system: every card now shares the exact
     announce / footer / nav tone (one source of truth for all page glass). */
  --card-bg: var(--glass-bg);
  --card-border: var(--glass-border);
  --card-border-hover: rgba(120, 160, 255, 0.36);
  --card-shadow: var(--glass-shadow);
  --card-shadow-hover:
    inset 0 1px 0 rgba(255, 255, 255, 0.15),
    0 28px 52px -28px rgba(0, 0, 0, 0.68),
    0 0 24px -28px rgba(42, 91, 255, 0.22);

  /* icon tile (42x42 royal glass plate, identical to the app) */
  --tile-bg:
    radial-gradient(135% 135% at 25% 12%, rgba(95, 135, 255, 0.18), transparent 64%),
    linear-gradient(180deg, rgba(18, 26, 54, 0.82), rgba(10, 16, 34, 0.9));
  --tile-border: rgba(150, 180, 255, 0.2);
  --icon-color: #d4e2ff;

  /* geometry */
  --r-card: 16px;
  --r-card-lg: 18px;
  --r-panel: 22px;
  --r-tile: 12px;
  --r-pill: 999px;

  /* type */
  --font-sans: 'Geist Variable', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --font-mono: 'Geist Mono Variable', ui-monospace, 'Cascadia Mono', Menlo, monospace;
  --font-serif: 'Instrument Serif', ui-serif, Georgia, serif;

  /* motion */
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --dur-1: 0.18s;
  --dur-2: 0.34s;

  /* scroll-reveal grammar. Promoted from .page-home (css/landing.css) so every
     subpage inherits one entrance system instead of redefining it. Values are
     byte-identical to the home originals - this widened the scope, it did not
     retune the motion. */
  --reveal-dur: 0.72s;
  --reveal-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --reveal-rise: 24px;
  --stagger-step: 65ms;

  /* layout */
  --container: 1200px;
  --nav-h: 64px;
  --section-gap: clamp(72px, 10vw, 136px);

  /* z-scale */
  --z-nav: 50;
  --z-menu: 80;
  /* Above the page, below a dialog. The storage notice is the only thing that
     wants this rung: on --z-overlay it shared the modal layer, and since it is
     appended to <body> last it painted OVER a dimmed booking modal that was
     trying to be the only thing on screen. */
  --z-notice: 90;
  --z-overlay: 100;
}

/* ---------- Reset / base ---------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  background-color: var(--bg-0);
  color: var(--text-hi);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden; /* fallback for engines without clip */
  overflow-x: clip;   /* clip contains horizontal bleed WITHOUT making <body> a scroll container - keeps position:sticky nav working on mobile */
}

/* ambient royal glow behind everything - cheap fixed gradients, no blur */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(68% 52% at 16% 8%, rgba(52, 104, 255, 0.13), transparent 60%),
    radial-gradient(58% 48% at 88% 16%, rgba(42, 91, 255, 0.11), transparent 58%),
    radial-gradient(52% 44% at 78% 62%, rgba(60, 110, 255, 0.09), transparent 60%),
    radial-gradient(60% 50% at 10% 74%, rgba(46, 96, 255, 0.08), transparent 60%),
    radial-gradient(46% 38% at 48% 42%, rgba(74, 122, 255, 0.05), transparent 66%),
    linear-gradient(180deg, #070c1a 0%, var(--bg-0) 40%);
}
/* fixed film grain over the aurora (port of the noise layer from 21st "Dark Gradient
   Background") — kills gradient banding and gives the ambient a premium, textured feel.
   Static, cheap, non-distracting. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.05;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 140px 140px;
}

img, svg, video { display: block; max-width: 100%; }
img { height: auto; }

h1, h2, h3, h4, p, ul, ol, figure { margin: 0; }
ul, ol { padding: 0; }

a {
  color: inherit;
  text-decoration: none;
  transition: color var(--dur-1) var(--ease-out);
}

button {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

/* The `hidden` attribute must win. A component that sets its own display - and
   most of ours do (grid, flex, inline-flex) - silently beats the browser's own
   `[hidden] { display: none }`, so `el.hidden = true` changed nothing. That has
   bitten this codebase twice: a dialog footer stayed as an 80px bordered strip,
   and a Book button stayed visible on an unbookable package. One rule here means
   no component has to remember to restate it. */
[hidden] { display: none !important; }

::selection { background: rgba(42, 91, 255, 0.4); color: #fff; }

:focus-visible {
  outline: 2px solid rgba(94, 140, 255, 0.85);
  outline-offset: 2px;
  border-radius: 4px;
}

/* scrollbar */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--bg-0); }
::-webkit-scrollbar-thumb {
  background: rgba(150, 180, 255, 0.16);
  border-radius: 8px;
  border: 2px solid var(--bg-0);
}
::-webkit-scrollbar-thumb:hover { background: rgba(150, 180, 255, 0.28); }

/* ---------- Slim scrollbar (.scroll-slim) ----------------------------------
   Promoted from css/services.css, where it was written for the booking dialog's
   calendar: "a classic scrollbar costs 15px of a 280px calendar - a whole column
   of thumb room on the narrowest phones." Every small scroll region on the site
   has that problem, so the recipe is a class now rather than two hand-named
   selectors. The 10px bar above is right for a page; this is for a box.

   .dp-body and .dp-times are named here so /services/ keeps it without a markup
   change, and they are the reason this lives in the shared file at all. */
.scroll-slim,
.dp-body,
.dp-times {
  scrollbar-width: thin;
  scrollbar-color: rgba(150, 180, 255, 0.18) transparent;
}
.scroll-slim::-webkit-scrollbar,
.dp-body::-webkit-scrollbar,
.dp-times::-webkit-scrollbar { width: 6px; height: 6px; }
.scroll-slim::-webkit-scrollbar-track,
.dp-body::-webkit-scrollbar-track,
.dp-times::-webkit-scrollbar-track { background: transparent; }
.scroll-slim::-webkit-scrollbar-thumb,
.dp-body::-webkit-scrollbar-thumb,
.dp-times::-webkit-scrollbar-thumb { background: rgba(150, 180, 255, 0.18); border-radius: 999px; }
.scroll-slim::-webkit-scrollbar-thumb:hover,
.dp-body::-webkit-scrollbar-thumb:hover,
.dp-times::-webkit-scrollbar-thumb:hover { background: rgba(150, 180, 255, 0.32); }

/* ---------- Tailwind-utility compat (REQUIRED by legacy inline JS) ---------- */
.hidden { display: none !important; }
.opacity-0 { opacity: 0; }
.opacity-100 { opacity: 1; }
.pointer-events-none { pointer-events: none; }
.pointer-events-auto { pointer-events: auto; }
.underline { text-decoration: underline; }
.text-primary { color: #7fa2ff; }
.mr-2 { margin-right: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mt-6 { margin-top: 1.5rem; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.gap-3 { gap: 0.75rem; }
.w-full { width: 100%; }
.space-y-3 > * + * { margin-top: 0.75rem; }
.leading-relaxed { line-height: 1.65; }
.font-bold { font-weight: 700; }
.tracking-tight { letter-spacing: -0.02em; }
.text-2xl { font-size: 1.5rem; line-height: 1.25; }
.rounded-2xl { border-radius: 1rem; }
.border { border-width: 1px; border-style: solid; }
.border-white\/10 { border-color: rgba(255, 255, 255, 0.1); }
.bg-surface\/90 { background-color: rgba(10, 13, 24, 0.92); }
.backdrop-blur-xl { backdrop-filter: blur(24px) saturate(160%); -webkit-backdrop-filter: blur(24px) saturate(160%); }
.shadow-glow { box-shadow: var(--glass-shadow); }
.text-white\/80 { color: rgba(240, 245, 255, 0.8); }
.transition-colors { transition: color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out); }
.duration-200 { transition-duration: 200ms; }
.hover\:text-white\/75:hover { color: rgba(255, 255, 255, 0.75); }
@media (min-width: 640px) {
  .sm\:flex-row { flex-direction: row; }
  .sm\:w-auto { width: auto; }
}
@media (min-width: 768px) {
  .md\:text-3xl { font-size: 1.875rem; line-height: 1.2; }
}

/* ---------- Primitives ---------- */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: clamp(18px, 4vw, 32px);
}

/* blurred liquid glass - BUDGET: max 3 simultaneously composited layers */
.glass {
  position: relative;
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
}
.glass::before {
  content: '';
  position: absolute;
  inset: 0 12% auto 12%;
  height: 1px;
  background: var(--glass-hairline);
  pointer-events: none;
}

/* royal card - the app's Advanced-tab card body, no blur.
   Carries a cursor-following spotlight: --spot is raised by :hover below, and
   --mx/--my are moved by js/card-glow.js. A page that loads the stylesheet but not
   that script still lights the glow on hover - it just sits at the card's centre. */
.card {
  --mx: 50%;
  --my: 50%;
  --spot: 0;
  position: relative;
  border-radius: var(--r-card-lg);
  border: 1px solid var(--card-border);
  background:
    radial-gradient(340px circle at var(--mx) var(--my), rgba(90, 130, 255, calc(var(--spot) * 0.9)), transparent 60%),
    var(--card-bg);
  box-shadow: var(--card-shadow);
  transition:
    transform var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out),
    box-shadow var(--dur-1) var(--ease-out);
}
.card-hover:hover, a.card:hover, .card:hover { --spot: 0.16; }
.card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 10%;
  right: 10%;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(190, 210, 255, 0.42), transparent);
  pointer-events: none;
}
a.card, .card-hover { cursor: pointer; }
.card-hover:hover, a.card:hover {
  transform: translateY(-2px);
  border-color: var(--card-border-hover);
  box-shadow: var(--card-shadow-hover);
}

/* island - faint glass tray behind card groups (no blur, perf) */
.island {
  border-radius: 24px;
  padding: clamp(14px, 2vw, 22px);
  border: 1px solid var(--island-border);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.022), rgba(255, 255, 255, 0.006)),
    radial-gradient(130% 120% at 0% -10%, rgba(42, 91, 255, 0.05), transparent 60%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045);
}

/* icon tile - 42x42 royal glass plate, lucide glyph 22/1.9 */
.icon-tile {
  width: 42px;
  height: 42px;
  border-radius: var(--r-tile);
  display: grid;
  place-items: center;
  flex-shrink: 0;
  overflow: hidden;
  border: 1px solid var(--tile-border);
  background: var(--tile-bg);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.13);
  color: var(--icon-color);
}
.icon-tile svg { width: 22px; height: 22px; }

/* eyebrow - mono uppercase kicker */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(150, 185, 255, 0.85);
}

/* badge chip */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 13px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-mid);
  background: linear-gradient(180deg, rgba(16, 22, 44, 0.55), rgba(10, 14, 30, 0.6));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-mid);
}

/* ---------- Buttons (glass recipes from the app) ---------- */
.btn {
  position: relative;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  min-height: 46px;
  padding: 11px 24px;
  border-radius: var(--r-pill);
  font-weight: 500;
  font-size: 0.95rem;
  letter-spacing: -0.005em;
  line-height: 1;
  cursor: pointer;
  white-space: nowrap;
  transition:
    transform var(--dur-1) var(--ease-out),
    border-color var(--dur-1) var(--ease-out),
    box-shadow var(--dur-1) var(--ease-out),
    background-color var(--dur-1) var(--ease-out),
    color var(--dur-1) var(--ease-out);
}
.btn svg { width: 18px; height: 18px; flex-shrink: 0; position: relative; }
.btn:active { transform: translateY(1px); }
/* liquid-glass: persistent top gloss (paints UNDER the label) */
.btn::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 44%);
  opacity: 0.8;
}
/* buttons carry no ::after decoration — hover is a calm border-brighten only (see the
   per-variant :hover rules below), matching the hero .xicon-cta. No sheen, no arrow. */
@keyframes btn-sheen {
  0% { left: -75%; opacity: 0; }
  22% { opacity: 0.9; }
  100% { left: 135%; opacity: 0; }
}
/* same liquid-glass sweep on the other prominent CTAs */
[data-discord-login], .cart-pop-checkout { position: relative; overflow: hidden; }
[data-discord-login]::after, .cart-pop-checkout::after {
  content: '';
  position: absolute;
  top: 0;
  left: -75%;
  width: 50%;
  height: 100%;
  pointer-events: none;
  transform: skewX(-16deg);
  background: linear-gradient(105deg, transparent, rgba(255, 255, 255, 0.26), transparent);
  opacity: 0;
}
[data-discord-login]:hover::after, .cart-pop-checkout:hover::after { animation: btn-sheen 0.85s var(--ease-out); }
[data-discord-login] > *, .cart-pop-checkout > * { position: relative; }

.btn-primary {
  color: #eaf0ff;
  border: 1px solid rgba(120, 155, 255, 0.42);
  background:
    radial-gradient(130% 150% at 0% 0%, rgba(90, 130, 255, 0.26), transparent 60%),
    linear-gradient(180deg, rgba(42, 91, 255, 0.42), rgba(30, 58, 174, 0.38));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.18),
    0 6px 16px -14px rgba(42, 91, 255, 0.3);
}
.btn-primary:hover {
  border-color: rgba(140, 170, 255, 0.6);
  background:
    radial-gradient(130% 150% at 0% 0%, rgba(100, 140, 255, 0.34), transparent 60%),
    linear-gradient(180deg, rgba(52, 100, 255, 0.5), rgba(36, 68, 196, 0.46));
}

.btn-ghost {
  color: var(--text-hi);
  border: 1px solid var(--border-mid);
  background: linear-gradient(180deg, rgba(16, 22, 44, 0.4), rgba(10, 14, 30, 0.46));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.btn-ghost:hover {
  border-color: rgba(130, 165, 255, 0.45);
}

/* opt-in expanding arrow (add class .btn-xarrow) for text-only navigational CTAs. Buttons
   that already carry an icon (Discord) or a static arrow (.shiny-cta, .flow-go) must NOT get
   it — no double glyphs. The arrow is a mask tinted with the button's own text color; the
   negative margin cancels the flex gap while collapsed, so there's no trailing space at rest. */
.btn-xarrow::after {
  content: '';
  --btn-arrow: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='2.2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M5%2012h14'/%3E%3Cpath%20d='m12%205%207%207-7%207'/%3E%3C/svg%3E") no-repeat center / contain;
  flex-shrink: 0;
  width: 0;
  height: 15px;
  margin-left: -9px;               /* cancel the flex gap while collapsed */
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  transform: translateX(4px);
  background-color: currentColor;
  -webkit-mask: var(--btn-arrow);
  mask: var(--btn-arrow);
  transition:
    width 0.25s var(--ease-out),
    margin 0.25s var(--ease-out),
    opacity 0.25s var(--ease-out),
    transform 0.25s var(--ease-out);
}
.btn-xarrow:hover::after,
.btn-xarrow:focus-visible::after {
  width: 15px;
  margin-left: 0;                  /* reveal the 9px flex gap */
  opacity: 1;
  transform: translateX(0);
}
@media (prefers-reduced-motion: reduce) {
  .btn-xarrow::after { transition: none; }
}

/* ---------- Header / floating glass nav ---------- */
header[data-nav-floating] {
  position: sticky;
  top: 14px;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  /* Odstep miedzy SEKCJAMI naglowka: logo | nawigacja | jezyk | koszyk |
     logowanie. Przy nawigacji z margin-inline:auto caly wolny zapas idzie w
     odstepy wokol niej, wiec ten gap widac przede wszystkim po PRAWEJ, gdzie
     jezyk, koszyk i przycisk stoja obok siebie bez zadnego innego oddzielenia.
     Przy 18 px czytalo sie to jak zbita grupa. 26 px kosztuje 40 px szerokosci
     w calym naglowku - stac nas na to dopiero odkad skrocona etykieta przycisku
     logowania oddala 93 px (patrz .nav-login-long nizej). */
  gap: 26px;
  width: min(var(--container), calc(100% - 28px));
  margin: 14px auto 0;
  min-height: var(--nav-h);
  padding: 10px 12px 10px 20px;
  border-radius: var(--r-card-lg);
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
  overflow: visible; /* auth.js anchors dropdowns here - never clip, never transform */
  transition: background-color var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease-out);
}
header[data-nav-floating]::before {
  content: '';
  position: absolute;
  top: 0;
  left: 12%;
  right: 12%;
  height: 1px;
  background: var(--glass-hairline);
  pointer-events: none;
}
header[data-nav-floating][data-scrolled] {
  background:
    radial-gradient(60% 100% at 6% 0%, rgba(190, 210, 255, 0.08), transparent 55%),
    linear-gradient(180deg, rgba(7, 10, 22, 0.82), rgba(4, 6, 14, 0.88));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 18px 40px -20px rgba(0, 0, 0, 0.7),
    0 0 44px -32px rgba(42, 91, 255, 0.2);
}

/* brand
   Matched by POSITION, not by the text of its aria-label. This used to be
   a[aria-label="Home"], which quietly stopped matching the moment /pl/ shipped
   and the label became "Strona glowna": the logo lost its flex row, its gap and
   its font size, and - via the mobile rule far below - the margin-right:auto
   that pushes the nav across. The whole header shifted about 37px and read as
   broken, on every Polish page, from one translated attribute.
   A selector must never depend on prose a translator is supposed to change. */
header[data-nav-floating] > a:first-child {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: 600;
  font-size: 1.02rem;
  letter-spacing: -0.01em;
  color: var(--text-hi);
  white-space: nowrap;
}
header[data-nav-floating] > a:first-child img {
  width: 26px;
  height: 26px;
  object-fit: contain;
}

/* primary nav */
header[data-nav-floating] nav {
  display: flex;
  align-items: center;
  /* 10, nie 4. Przy 4 px pigulka aktywnej pozycji stala 19 px od sasiedniego
     napisu (4 px odstepu + 15 px jej wlasnego paddingu) - a odkad strzalki
     rozwijanych menu przestaly spadac pod tekst i zaczely zajmowac swoje 14 px
     w linii, ten odstep czytal sie jak sciscisk. Zmierzone: przy 10 px zostaje
     19 px zapasu w najwezszym naglowku, przy ktorym menu jest jeszcze widoczne
     (983 px; nizej chowa sie na rzecz mobilnego), i 236 px przy szerokim.
     Powyzej 12 px zapas schodzi do 11 px i naglowek robi sie ryzykowny. */
  gap: 10px;
  margin-inline: auto;
}
a[data-nav-link] {
  padding: 9px 15px;
  border-radius: var(--r-pill);
  font-size: 0.92rem;
  font-weight: 500;
  color: var(--text-mid);
  border: 1px solid transparent;
  /* Strzalka rozwijanego menu (a[data-nav-menu]::after) to inline-block w tresci
     tego linku, a tresc miescila sie w nim co do PIKSELA - zmierzone na zywo:
     pole tresci 77,11 px, tekst 63,11 + strzalka 14,00 = 77,11, zapas 0,00.
     Przy flex-shrink 1 wystarczylo, zeby naglowek zrobil sie odrobine ciasniejszy
     - dluzszy polski przycisk logowania, inna czcionka zapasowa jeszcze przed
     wczytaniem webfonta - i strzalka spadala pod tekst, rozpychajac pigulke z 44
     na 67 px. nowrap czyni zlamanie niemozliwym, a flex:none nie pozwala scisnac
     linku ponizej jego wlasnej szerokosci. */
  white-space: nowrap;
  flex: none;
  transition: color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
a[data-nav-link]:hover {
  color: var(--text-hi);
  background: rgba(150, 180, 255, 0.07);
}
a[data-nav-link][aria-current="page"] {
  color: #dfe8ff;
  background:
    radial-gradient(130% 150% at 0% 0%, rgba(42, 91, 255, 0.3), transparent 60%),
    linear-gradient(180deg, rgba(42, 91, 255, 0.16), rgba(30, 58, 174, 0.14));
  border-color: rgba(120, 155, 255, 0.28);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
}

/* language switcher */
/* Not id-scoped: .i18n-trigger is unique to the switcher, and the id put this at
   1-1-0 where it beat [data-sel-btn][aria-expanded="true"] (0-2-0) - so the one
   trigger on the site that most needed an open state was the one that could not
   receive it. */
.i18n-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  /* Icon only now, so the pill is square rather than sized to two letters. */
  width: 44px;
  min-width: 44px;
  min-height: 44px;      /* 38px was under the size a finger reliably hits */
  padding: 0;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-mid);
  background: linear-gradient(180deg, rgba(16, 22, 44, 0.4), rgba(10, 14, 30, 0.46));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
  color: var(--text-mid);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.i18n-trigger:hover {
  color: var(--text-hi);
  border-color: rgba(120, 160, 255, 0.36);
}
/* The switcher's popup IS the shared .s-sel-list now; only its geometry differs.
   The list defaults to left:0;right:0, which spans its trigger - correct for a
   full-width form field, and 40px wide under an icon button. */
#i18n-switcher .i18n-list {
  /* The ONLY differences an icon trigger justifies. .s-sel-list stretches to its
     trigger, which under a 44px button would be a 44px list. `top` and z-index
     used to be restated here too and were drift - the z-index is now correct in
     the shared rule instead. */
  left: auto;
  min-width: 152px;
}
/* The row is the shared .s-sel-opt. Everything this used to restate - padding,
   radius, font-size, colour, cursor, the hover background, the chosen-row
   gradient - was drift, and being id-scoped it beat the shared rule everywhere,
   including inside @media (pointer: coarse), so this was the one dropdown on the
   site whose rows never grew for touch. The hover override was worse: it
   redefined :hover but not .is-active, so a row lit by the arrow keys DIMMED
   when the mouse reached it - the exact disagreement the shared rule exists to
   prevent. All of it is gone; only the layout the tick needs stays. */
#i18n-switcher .i18n-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
#i18n-switcher .i18n-opt[aria-selected="true"]::after {
  content: "";
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
}
#i18n-switcher .i18n-opt[aria-selected="true"] {
  color: #dfe8ff;
  background:
    radial-gradient(130% 150% at 0% 0%, rgba(42, 91, 255, 0.3), transparent 60%),
    linear-gradient(180deg, rgba(42, 91, 255, 0.18), rgba(30, 58, 174, 0.16));
}

/* cart link in header */
[data-nav-cart] {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* Square, to match the language button beside it. The mobile-menu copy of
     this link keeps its label and is exempt below. */
  width: 44px;
  min-height: 44px;
  padding: 0;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-mid);
  background: linear-gradient(180deg, rgba(16, 22, 44, 0.4), rgba(10, 14, 30, 0.46));
  font-size: 0.88rem;
  font-weight: 500;
  color: var(--text-mid);
  transition: color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
[data-nav-cart]:hover { color: var(--text-hi); border-color: rgba(120, 160, 255, 0.36); }
/* the mobile-menu copy keeps its label, so its type stays there, not here */
.cart-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 19px;
  height: 19px;
  padding: 0 5px;
  border-radius: var(--r-pill);
  background: linear-gradient(180deg, rgba(62, 108, 255, 0.95), rgba(36, 72, 214, 0.95));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 0 12px -4px rgba(42, 91, 255, 0.8);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 600;
  color: #f2f6ff;
}
/* On the square icon button, the count is a marker ON the button rather than a
   second thing inside it. As a flex sibling of the glyph it widened the 44px
   square, pushed the glyph off its centre and left the cart wider than the
   language button beside it, so the nav row lost its rhythm. Pinned to the corner
   the button keeps its size and the icon stays centred.

   Scoped with :has(.cart-ico) because the copy inside the mobile menu keeps its
   text label - there the badge belongs inline, after the word. A browser without
   :has() simply keeps the old inline placement, which is the state this replaces. */
[data-nav-cart]:has(.cart-ico) .cart-badge {
  position: absolute;
  top: 1px;
  right: 1px;
  /* A dark rim, not a cut-out matching the page: the nav is glass and whatever is
     behind it scrolls, so the ring has to be its own colour. */
  border: 2px solid rgba(7, 10, 22, 0.92);
}
/* A dot while the cart holds one thing - see updateCartBadge(). Both writers
   replace the items array with exactly one entry, so a digit here could only ever
   read "1": a counter that cannot count. The number returns on its own the day the
   cart can hold two. */
[data-nav-cart]:has(.cart-ico) .cart-badge.is-dot {
  min-width: 0;
  /* border-box, so the 2px rim eats into this: 14px leaves 10px of royal, which is
     the dot you actually see. At 10px total there were 6px left and it read as a
     ring with a speck in it. */
  width: 14px;
  height: 14px;
  padding: 0;
  top: 2px;
  right: 2px;
}

/* Discord login button (logged-out) */
/* Icons in the nav never absorb a squeeze. A flex child shrinks by default, and
   when the floating header morphs to its 940px scrolled width the login button
   loses about 15px - all of which came out of the Discord mark, collapsing it from
   18px to 2.6px: present in the DOM, invisible on screen. Text can wrap or ellipsis
   under pressure; a glyph has nothing to give. */
header[data-nav-floating] svg,
[data-discord-login] svg,
[data-nav-cart] svg,
.i18n-trigger svg { flex-shrink: 0; }

[data-discord-login] {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-height: 42px;
  padding: 9px 19px;
  border-radius: var(--r-pill);
  border: 1px solid rgba(120, 155, 255, 0.42);
  background:
    radial-gradient(130% 150% at 0% 0%, rgba(90, 130, 255, 0.26), transparent 60%),
    linear-gradient(180deg, rgba(42, 91, 255, 0.42), rgba(30, 58, 174, 0.38));
  /* inset top highlight only: the outer royal glow is gone so the nav stays flat */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18);
  font-size: 0.9rem;
  font-weight: 500;
  color: #eaf0ff;
  cursor: pointer;
  white-space: nowrap;
  transition: transform var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out);
}
/* hover changes the border only: no lift, no glow */
[data-discord-login]:hover {
  border-color: rgba(140, 170, 255, 0.6);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
[data-discord-login] svg, [data-discord-login] i { width: 18px; height: 18px; }

/* logged-in state (auth.js swaps innerHTML) */
[data-discord-login].logged-in {
  padding: 6px 14px 6px 8px;
  background: linear-gradient(180deg, rgba(16, 22, 44, 0.5), rgba(10, 14, 30, 0.56));
  border-color: var(--border-mid);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
[data-discord-login].logged-in:hover { border-color: rgba(120, 160, 255, 0.36); transform: none; }
.discord-user-content { display: inline-flex; align-items: center; gap: 9px; }
.discord-user-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--border-strong);
}
.discord-user-avatar-fallback {
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  color: var(--icon-color);
}
/* The caret that replaced the username. It turns over when the menu opens, so
   the control reports its own state instead of leaving you to guess. */
.user-caret {
  display: inline-grid;
  place-items: center;
  color: var(--text-low);
  transition: transform var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
[data-discord-login].logged-in:hover .user-caret { color: var(--text-mid); }
.user-menu-wrap.is-open .user-caret { transform: rotate(180deg); color: var(--text-mid); }
/* Tighter than the old name-plus-avatar pill: an avatar and a caret only. */
[data-discord-login].logged-in { padding: 6px 12px 6px 7px; }

/* Icon-only nav buttons: the glyph carries the whole control, so it is centred
   and slightly brighter than a label would be. */
.cart-ico { display: inline-grid; place-items: center; }
.cart-ico svg, #i18n-switcher .i18n-current svg { display: block; }
#i18n-switcher .i18n-current { display: inline-grid; place-items: center; }

/* No rule for the mobile menu's cart link: auth.js builds that one with
   data-mobile-menu-cart, not data-nav-cart, so it never matched this selector and
   never picked up the icon either. It is a plain labelled row styled with the
   other links in #mobile-menu. */

/* user dropdown menu (auth.js) */
.user-menu-wrap { position: relative; display: inline-flex; }
.user-menu {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  min-width: 200px;
  padding: 6px;
  border-radius: 14px;
  border: 1px solid var(--card-border);
  background:
    radial-gradient(120% 150% at 100% 0%, rgba(42, 91, 255, 0.14), transparent 55%),
    linear-gradient(180deg, rgba(13, 18, 38, 0.97), rgba(8, 12, 26, 0.98));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 22px 44px -20px rgba(0, 0, 0, 0.75);
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  transition: opacity var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out);
  z-index: var(--z-menu);
}
.user-menu-wrap.is-open .user-menu {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
/* Who you are signed in as. The trigger stopped saying it, so this is now the
   only place the name appears - it leads the menu rather than sitting in it. */
.user-menu-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 13px 12px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border-soft);
}
.user-menu-hint {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-low);
}
.user-menu-name {
  font-size: 0.96rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-hi);
  overflow-wrap: anywhere;
}

.user-menu a, .user-menu button {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 13px;
  border-radius: 9px;
  font-size: 0.9rem;
  color: var(--text-mid);
  cursor: pointer;
  text-align: left;
  transition: color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
}
.user-menu a:hover, .user-menu button:hover {
  color: var(--text-hi);
  background: rgba(150, 180, 255, 0.08);
}
.user-menu svg, .user-menu i { width: 17px; height: 17px; flex-shrink: 0; opacity: 0.8; }

/* mobile menu button */
#mobile-menu-btn {
  display: none;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border-radius: var(--r-tile);
  border: 1px solid var(--border-mid);
  background: linear-gradient(180deg, rgba(16, 22, 44, 0.4), rgba(10, 14, 30, 0.46));
  color: var(--text-hi);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease-out);
}
#mobile-menu-btn:hover { border-color: rgba(120, 160, 255, 0.36); }
#mobile-menu-btn svg { width: 20px; height: 20px; }

/* mobile menu overlay (portaled to <body> by auth.js) */
#mobile-menu {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 32px 24px;
  background:
    radial-gradient(90% 60% at 50% -10%, rgba(42, 91, 255, 0.16), transparent 60%),
    linear-gradient(180deg, rgba(5, 8, 18, 0.96), rgba(2, 3, 10, 0.98));
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  transition: opacity 0.25s var(--ease-out);
}
#mobile-menu a {
  padding: 13px 26px;
  border-radius: var(--r-pill);
  font-size: 1.25rem;
  font-weight: 500;
  color: var(--text-mid);
  transition: color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
}
#mobile-menu a:hover { color: var(--text-hi); background: rgba(150, 180, 255, 0.07); }
#mobile-menu [data-discord-login] { margin-top: 18px; }
#mobile-menu-close {
  position: absolute;
  top: 20px;
  right: 20px;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: var(--r-tile);
  border: 1px solid var(--border-mid);
  background: linear-gradient(180deg, rgba(16, 22, 44, 0.5), rgba(10, 14, 30, 0.56));
  color: var(--text-hi);
  cursor: pointer;
}
#mobile-menu-close svg { width: 20px; height: 20px; }

/* Prog, ponizej ktorego menu desktopowe ustepuje mobilnemu.

   1119, nie 1023. Zmierzone: naglowek ze wszystkimi sekcjami (logo 162 +
   nawigacja 478 + jezyk 44 + koszyk 44 + przycisk 108, plus padding i odstepy)
   potrzebuje 970 px i siada na tym minimum przy oknie 998 px. Zeby marginesy
   automatyczne wokol nawigacji mialy chocby po 40 px z kazdej strony, okno musi
   miec 1078 px. Przy starym progu 1023 px pokazywalismy wiec pelne menu w pasmie
   1024-1078, w ktorym NIE BYLO NA NIE MIEJSCA - logo dotykalo pierwszej pigulki,
   a jezyk, koszyk i przycisk zbijaly sie w jedna grupe. Menu mobilne dziala tam
   poprawnie i wyglada lepiej niz scisniete desktopowe.

   Ta liczba MUSI zgadzac sie z MOBILE_BREAKPOINT w js/auth.js. CSS uzywa
   max-width (wlacznie), JS porownuje <=, wiec obie strony daja to samo: menu
   mobilne do 1119 px, desktopowe od 1120 px. */
@media (max-width: 1119px) {
  header[data-nav-floating] nav { display: none; }
  header[data-nav-floating] { gap: 10px; padding-right: 10px; }
  header[data-nav-floating] > a:first-child { margin-right: auto; }
  #mobile-menu-btn { display: inline-flex; }
  header[data-nav-floating] [data-discord-login] { display: none; }
}
@media (max-width: 479px) {
  /* Was min-width 42px + padding 8px 10px, sized for the "EN" label. The
     trigger is a centred icon now, so it only needs to be a touch smaller. */
  .i18n-trigger { width: 42px; min-width: 42px; min-height: 42px; }
  [data-nav-cart] { width: 42px; min-height: 42px; }
}

/* ---------- Cart popover (auth.js injected) ---------- */
.cart-pop-wrap { position: relative; display: inline-flex; }
.cart-popover {
  position: absolute;
  top: calc(100% + 12px);
  right: 0;
  width: 320px;
  border-radius: var(--r-card);
  border: 1px solid var(--card-border);
  background:
    radial-gradient(120% 150% at 100% 0%, rgba(42, 91, 255, 0.16), transparent 55%),
    linear-gradient(180deg, rgba(13, 18, 38, 0.97), rgba(8, 12, 26, 0.98));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 26px 52px -22px rgba(0, 0, 0, 0.78),
    0 0 34px -26px rgba(42, 91, 255, 0.3);
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  transition: opacity var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out);
  z-index: var(--z-menu);
}
.cart-pop-wrap.is-open .cart-popover {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
.cart-pop-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px 10px;
}
.cart-pop-title {
  font-size: 0.78rem;
  font-family: var(--font-mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-low);
}
.cart-pop-close, .cart-pop-remove {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 9px;
  border: 1px solid transparent;
  color: var(--text-low);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
}
.cart-pop-close:hover, .cart-pop-remove:hover {
  color: var(--text-hi);
  border-color: var(--border-mid);
  background: rgba(150, 180, 255, 0.07);
}
.cart-pop-close svg, .cart-pop-remove svg,
.cart-pop-close i, .cart-pop-remove i { width: 16px; height: 16px; }
.cart-pop-body { padding: 4px 16px 12px; }
.cart-pop-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px;
  border-radius: 13px;
  border: 1px solid var(--border-soft);
  background: linear-gradient(180deg, rgba(16, 22, 44, 0.4), rgba(10, 14, 30, 0.46));
}
.cart-pop-thumb {
  width: 46px;
  height: 46px;
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid var(--border-mid);
  background: var(--bg-2);
  flex-shrink: 0;
}
.cart-pop-thumb img { width: 100%; height: 100%; object-fit: cover; }
.cart-pop-info { flex: 1; min-width: 0; }
.cart-pop-name {
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--text-hi);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cart-pop-price {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: rgba(150, 185, 255, 0.9);
  margin-top: 2px;
}
.cart-pop-link {
  display: inline-block;
  margin-top: 4px;
  font-size: 0.78rem;
  color: var(--text-low);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.cart-pop-link:hover { color: var(--text-hi); }
.cart-pop-empty {
  padding: 18px 4px;
  text-align: center;
  font-size: 0.88rem;
  color: var(--text-low);
}
.cart-pop-actions { padding: 0 16px 16px; }
/* Was a hand-painted copy of .btn + .btn-primary - same border colour, the same
   two gradients, the same inset highlight and the same text colour, differing only
   by an accident of drift (42px tall not 46, 0.9rem not 0.95rem, a heavier outer
   shadow, and its own hover). It now wears the shared classes, so it tracks the
   button family instead of slowly diverging from it, and it inherits the focus
   ring and the active press it never had.

   What stays local: filling the popover's width, because .btn is inline-flex; and
   the class itself as the marker that keeps this button in the sheen list above,
   which .btn deliberately does not carry. */
.cart-pop-checkout { width: 100%; }

/* ---------- Footer ---------- */
#site-footer {
  margin-top: var(--section-gap);
  /* same liquid-glass tone as the nav / cards / announce so it belongs to the set
     (translucent: the aurora + stars still show through). */
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  padding: clamp(36px, 6vw, 64px) clamp(18px, 4vw, 32px) 32px;
}
#site-footer nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px 26px;
  max-width: var(--container);
  margin: 0 auto 26px;
}
#site-footer nav a {
  padding: 6px 2px;
  font-size: 0.9rem;
  color: var(--text-mid);
}
#site-footer nav a:hover { color: var(--text-hi); }
#site-footer p {
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  letter-spacing: 0.08em;
  color: var(--text-low);
}

/* ---------- Section rhythm ----------
   Promoted verbatim from `.page-home .section*` (css/landing.css) so any subpage
   gets the vertical rhythm and heading scale without carrying the home layer.
   Home keeps its own leaner --section-gap override, so its spacing is unchanged. */
.section { margin-top: var(--section-gap); }
.section-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  text-align: center;
  max-width: 720px;
  margin-inline: auto;
  margin-bottom: clamp(30px, 4.5vw, 52px);
}
.section-head h2 {
  font-size: clamp(1.7rem, 3.6vw, 2.5rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.12;
  color: var(--text-hi);
}
.section-sub {
  font-size: 1.02rem;
  line-height: 1.65;
  color: var(--text-mid);
  max-width: 58ch;
}
/* Heading accent: serif-italic royal gradient on the <em> payoff phrase. */
.section-head h2 em {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  letter-spacing: -0.012em;
  background: linear-gradient(115deg, #d4e2ff 8%, #7fa2ff 54%, var(--accent) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

/* ---------- Scroll reveal ----------
   Promoted from `.page-home .reveal*` (css/landing.css). The observer in
   js/landing.js was already page-agnostic; only the CSS was locked to home.
   .reveal        observer hook (JS adds .is-visible)
   .reveal-group  stagger host: its .reveal-item children arrive in sequence
   .reveal-item   staggered child, driven by an ANIMATION on purpose. a transition
                  shorthand here would silently wipe the element's own hover
                  transition, and hover behaviour must stay exactly as it is.
   .reveal-still  container is a trigger only, it does not travel itself
   .reveal-fade   fade with no travel, for things that already move */
.reveal {
  opacity: 0;
  transform: translateY(var(--reveal-rise));
  transition: opacity var(--reveal-dur) var(--reveal-ease),
              transform var(--reveal-dur) var(--reveal-ease);
}
.reveal.is-visible {
  opacity: 1;
  transform: none;
}
/* fade only. must sit after .is-visible: equal specificity, later wins */
.reveal-fade { --reveal-rise: 0px; }
.reveal.reveal-still {
  opacity: 1;
  transform: none;
  transition: none;
}
.reveal-group .reveal-item { opacity: 0; }
.reveal-group.is-visible .reveal-item {
  animation: flow-card-in var(--reveal-dur) var(--reveal-ease) both;
  animation-delay: calc(var(--stagger-lead, 0ms) + var(--stagger-step) * var(--i, 0));
}
/* --reveal-end lets a card rest at something other than fully opaque. A card that
   is deliberately dimmed - "not booking yet" - cannot express that with its own
   opacity declaration, because this animation's `both` fill would overwrite it the
   moment the card is revealed, and its parked opacity: 0 would lose the cascade to
   it beforehand. Declaring the resting value here keeps one property under one
   owner. */
@keyframes flow-card-in {
  from { opacity: 0; transform: translateY(var(--reveal-rise, 14px)); }
  to { opacity: var(--reveal-end, 1); transform: none; }
}

/* -- Hero entrance --------------------------------------------------------
   The same picture as .reveal-item - same keyframes, same tokens, same stagger
   step - on a LOAD trigger instead of a scroll one.

   A hero sits above the fold, so an IntersectionObserver buys nothing there: it
   fires on its first callback anyway, and in exchange the page's largest text
   becomes invisible until a script runs. Below the fold, .reveal-item; above
   it, this. One system, one keyframe, two triggers - which is the whole point.

   --i SHIPS IN THE MARKUP (style="--i:0", "--i:1", ...) and is written by the
   page builders. It used to be set by js/reveal.js after load, which was wrong
   in a way that only showed sometimes: with no --i the calc() resolves to a 0ms
   delay, so every item started together, and the script then changed
   animation-delay on animations that were already running. Re-timing a running
   animation with `fill: both` throws an item that had faded in back into its
   delay phase - opacity 0 - and it fades in again. Hence the flicker.

   In the markup it is correct from the first frame, and the hero still needs no
   JS at all to become visible. */
.hero-seq .hero-item {
  animation: flow-card-in var(--reveal-dur) var(--reveal-ease) both;
  animation-delay: calc(var(--stagger-lead, 0ms) + var(--stagger-step) * var(--i, 0));
}
@media (prefers-reduced-motion: reduce) {
  /* The scroll half honours this in JS, by revealing everything at once. A
     load-time entrance has no script in its path, so it is cancelled here. */
  .hero-seq .hero-item { animation: none; }
}


/* ---------- Footer enrichment (moved from css/landing.css) ----------
   The brand column, link grid, glowing top hairline and the flicker canvas were
   locked to .page-home, which left every subpage with the bare nav+copyright
   footer. De-scoped here so the whole site shares one footer. Values verbatim. */

#site-footer { position: relative; overflow: hidden; padding-bottom: clamp(20px, 2.5vw, 30px); }
/* glowing glass hairline centered on the top edge (premium capstone accent) */
.footer-edge {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(440px, 62%);
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(130, 170, 255, 0.75), transparent);
  box-shadow: 0 0 14px 1px rgba(42, 91, 255, 0.5);
  pointer-events: none;
  z-index: 2;
}
#site-footer::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 60%;
  height: 160px;
  pointer-events: none;
  background: radial-gradient(50% 100% at 50% 0%, rgba(42, 91, 255, 0.12), transparent 70%);
}

.footer-top {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 1.15fr 2fr;
  gap: clamp(30px, 5vw, 68px);
  max-width: var(--container);
  margin: 0 auto;
  align-items: start;
}
.footer-brand {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  max-width: 340px;
}
.footer-logo {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--text-hi);
}
#site-footer .footer-tag {
  text-align: left;
  font-size: 0.9rem;
  line-height: 1.55;
  color: var(--text-mid);
}
/* The brand column's social row - replaced the single "Join the Discord" pill.
   Five labelled pills down a 340px column would have been a wall of text, so the
   marks do the naming. One plate, one glyph colour, five places: the row reads as
   a set. Discord leads by sitting first, nothing else. */
.footer-social {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 2px;
  list-style: none;
}
.footer-soc {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--r-tile);
  border: 1px solid var(--border-mid);
  background: linear-gradient(180deg, rgba(16, 22, 44, 0.5), rgba(10, 14, 30, 0.56));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
  color: #8fb0ff;
  transition: border-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.footer-soc svg { display: block; }
/* Icon-only, so the glyph is half the affordance and brightens with the border.
   No lift and no glow - the same restraint as the pill this replaces, which moved
   only its border so the two Discord CTAs on a page behaved alike. */
.footer-soc:hover { border-color: rgba(130, 165, 255, 0.45); color: #dde7ff; }

.footer-links {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(18px, 3vw, 40px);
}
.footer-col h3 {
  margin-bottom: 15px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(150, 185, 255, 0.7);
}
.footer-col ul { list-style: none; display: flex; flex-direction: column; gap: 11px; }
/* .footer-linkbtn is a button, not a link, because it opens the storage panel
   rather than going anywhere - but it sits in a list of links and has to read as
   one of them, so it takes these declarations rather than a rule of its own. It
   used to have one, carrying `font: inherit`, which handed it the body's 16px
   against these links' 0.9rem and the brighter inherited colour: the single
   control in the footer sat a size larger and a shade lighter than everything
   around it. Unscoped on purpose - the same button appears in the plain footer
   on /PrivacyPolicy/ and /TermsOfService/, which has no .footer-col. */
.footer-col a,
.footer-linkbtn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.9rem;
  color: var(--text-mid);
  transition: color var(--dur-1) var(--ease-out);
}
.footer-col a:hover,
.footer-linkbtn:hover { color: #dde7ff; }
.footer-col a svg { flex-shrink: 0; color: rgba(150, 185, 255, 0.6); }

.footer-bottom {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px 20px;
  max-width: var(--container);
  margin: clamp(30px, 4vw, 48px) auto 0;
  padding-top: 20px;
  border-top: 1px solid var(--border-soft);
}
#site-footer .footer-bottom p {
  margin: 0;
  text-align: left;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  letter-spacing: 0.06em;
  color: var(--text-low);
}

@media (max-width: 900px) {
  .footer-top { grid-template-columns: 1fr; gap: 34px; }
  .footer-links { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 26px 24px; }
}

/* Touch targets. These links render 23px tall and .footer-linkbtn 18px, against
   a 24px floor (WCAG 2.2, 2.5.8) and a 44px practical one - twelve of the
   eighteen controls down here failed both, on every page of the site.

   min-height rather than padding, because padding would have to be paired with a
   negative margin to keep the visual rhythm, and overlapping hit areas on a
   34px pitch cause mis-taps instead of curing them. The row gap comes down so
   the taller rows do not simply stack: pitch goes 34px -> 46px, which grows the
   footer by roughly one line per column and nothing else.

   pointer: coarse, not a width. A finger is the same size on a phone and on a
   1024px tablet, and a narrow desktop window has a mouse. */
@media (pointer: coarse) {
  .footer-col ul { gap: 2px; }
  .footer-col a,
  .footer-linkbtn { min-height: 44px; }
  .footer-soc { width: 44px; height: 44px; }
}
/* flickering-dot texture spanning the WHOLE footer (js/footer-fx.js), with the
   PROSPIRA wordmark revealed on the right. Sits behind the footer content; the top
   fades out so the dots melt into the page above (Atlas-style subtle texture). */
.footer-fx {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 14%, #000 100%);
          mask-image: linear-gradient(180deg, transparent 0%, #000 14%, #000 100%);
}
.footer-fx canvas { display: block; width: 100%; height: 100%; }
@media (max-width: 767px) {
  /* mobile: the wordmark sits in a strip BELOW the copyright bar, so give it room */
  #site-footer { padding-bottom: clamp(78px, 22vw, 104px); }
}

/* ---------- Page background layer (moved from css/landing.css) ----------
   The fixed starfield (.page-stars, js/sparkles.js) and the per-section canvas
   backdrops (.has-bgfx/.bgfx, js/section-bgfx.js). Also page-agnostic now. */

/* Static canvas backdrops painted once by js/section-bgfx.js (no animation -
   the only moving background stays the hero shader). The canvas bleeds a
   little past its section and fades out via mask so it melts into the page. */
/* fixed, whole-page animated starfield (js/sparkles.js, canvas[data-sparkles]),
   sitting over the aurora and under the grain + content. */
.page-stars { position: fixed; inset: 0; z-index: -1; pointer-events: none; }
.page-stars canvas { width: 100%; height: 100%; display: block; }
.has-bgfx { position: relative; isolation: isolate; }
.bgfx {
  position: absolute;
  inset: clamp(-96px, -7vw, -48px) 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 18%, #000 82%, transparent 100%);
  mask-image: linear-gradient(180deg, transparent 0%, #000 18%, #000 82%, transparent 100%);
}
.bgfx canvas {
  width: 100%;
  height: 100%;
  display: block;
}

@media (max-width: 768px) {
  .bgfx { inset: -40px 0; }
}

/* ---------- Navigation behaviour (moved from css/landing.css) ----------
   The morph and the sticky-nav overflow fix were locked to .page-home, so
   /utility/ rendered a permanent glass pill instead of the home page bar.
   Values verbatim; only the scope changed. */
/* ---------- Nav morph: flat transparent bar at the very top,
   collapsing into the floating glass pill once you scroll. Uses the existing
   data-scrolled toggle (js/landing.js navScrollState).

   SCOPE: every header carrying [data-nav-floating], and nothing else. This used
   to be a hand-kept list of body classes meaning "the pages that load
   js/nav-scroll.js", and it caught three pages in a row - /contact/, then both
   legal pages, then /404.html - each shipping with the glass pill stuck closed
   at the top of the page because adding a page meant remembering to come here
   and add it too. The list was only ever a restatement of something the markup
   already says, so the markup says it now.

   The invariant that remains: a header with [data-nav-floating] MUST load
   js/nav-scroll.js, or nothing ever sets [data-scrolled] and the header stays
   permanently transparent. That one is checked by check-pages.py rather than
   remembered. ---------- */
header[data-nav-floating] {
  width: min(var(--container), calc(100% - 28px));
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  transition:
    width var(--dur-2) var(--ease-out),
    padding var(--dur-2) var(--ease-out);
}
/* The glass pill (blur + tint + border + shadow) lives on ::after so it fades in as
   one smooth panel via opacity. NOTE: the mobile "nav slides away on scroll" bug was
   NOT this backdrop-filter - the real cause was overflow on <body> (an ancestor of
   the sticky <header>), fixed in the "Sticky-nav mobile fix" block below. With that
   fixed, the frosted pill is safe on every device, so the live glass is restored. */
header[data-nav-floating]::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  border: 1px solid var(--glass-border);
  background:
    radial-gradient(60% 100% at 6% 0%, rgba(190, 210, 255, 0.08), transparent 55%),
    linear-gradient(180deg, rgba(7, 10, 22, 0.82), rgba(4, 6, 14, 0.88));
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 18px 40px -20px rgba(0, 0, 0, 0.7),
    0 0 44px -32px rgba(42, 91, 255, 0.2);
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease-out);
  pointer-events: none;
}
/* the pill's top hairline only reads once the pill exists (scrolled) */
header[data-nav-floating]::before {
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease-out);
}
header[data-nav-floating][data-scrolled] {
  /* A pixel width, because the collapse has to ANIMATE. This transitions from the
     1200px bar above, and a transition cannot interpolate to an intrinsic size -
     max-content here made the width jump instead of travel, so the bar stopped
     folding into the pill and the pill just faded onto the page.

     940 is the original number and it is kept. It used to be 34px short of the
     signed-out row, and the shortfall came out of whatever would yield: the
     Discord glyph collapsed to under 3px and the wordmark pressed into the links.
     What buys the room now is the tightening below rather than a wider pill - the
     row needs about 916px at this spacing, so it fits with a little to spare,
     signed in or out. */
  width: min(940px, calc(100% - 28px));
  /* Tighter than the resting bar on every axis: the gap between blocks, the padding
     around them, and the padding inside each link further down. This is what makes
     940 enough, and it is also what makes the collapse read as a collapse. */
  gap: 12px;
  padding: 8px 10px 8px 16px;
  background: transparent;   /* keep the element clear; glass is the ::after panel */
  box-shadow: none;
}
/* The links carry most of the row's width - five of them at 15px a side - so this
   is where tightening pays. Held at 12px rather than less: below that the active
   pill around "General" starts to crowd its own label. */
header[data-nav-floating][data-scrolled] a[data-nav-link] {
  padding: 8px 12px;
}
header[data-nav-floating][data-scrolled]::after { opacity: 1; }
header[data-nav-floating][data-scrolled]::before { opacity: 1; }

/* ---------- Sticky-nav mobile fix ----------
   Scoped with :has() to the same condition as the block above - a body whose
   direct child is a floating header - because "every page with a sticky nav" is
   precisely what this rule is for, and that is a fact about the markup rather
   than a list of page names to keep in step.
   Mobile Chrome / Samsung Internet break position:sticky when ANY ancestor of the
   sticky element has overflow other than visible - INCLUDING overflow:clip. Desktop
   tolerates it; mobile does not (the nav slides up and vanishes during touch scroll,
   which is exactly what was happening even though getBoundingClientRect read top:14).
   The base stylesheet puts overflow-x on <body>, an ancestor of <header>. So on these
   pages we clear <body> overflow entirely and instead contain the horizontal
   bleed (champion glow, testimonials marquee, footer wordmark) on <main> and
   <footer> - which are siblings of <header>, never its ancestors. Sticky then runs
   on the compositor natively on every device. */
body:has(> header[data-nav-floating]) { overflow: visible; }                        /* body: no overflow on the sticky ancestor chain */
body:has(> header[data-nav-floating]) > main,
body:has(> header[data-nav-floating]) > footer { overflow-x: clip; }                /* contain the horizontal bleed here instead */


/* ---------- FAQ skin (moved from css/landing.css) ----------
   The accordion chrome: rounded entry, the +/x toggle that turns 45deg when
   open, the data-cat chip on the right and the answer indent that lines up
   under the question. De-scoped from .page-home so subpages share one FAQ.
   Behaviour (smooth height) comes from smoothFaq in js/landing.js, which
   already keys off details.home-faq-entry / .home-faq-answer. */
.faq-inner { max-width: 860px; }
.home-faq-wrap {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
/* height animation contract: ALL vertical padding lives on summary +
   .home-faq-answer; <details> itself only carries chrome */
.home-faq-entry {
  position: relative;
  border-radius: var(--r-card-lg);
  border: 1px solid var(--card-border);
  background: var(--card-bg);
  box-shadow: var(--card-shadow);
  overflow: hidden;
  transition:
    border-color var(--dur-2) var(--ease-out),
    transform var(--dur-2) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out);
}
/* royal glow that follows the cursor (js/landing.js magicCards feeds --mx/--my);
   falls back to a soft top-left wash before the pointer ever enters the card */
.home-faq-entry::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(300px circle at var(--mx, 14%) var(--my, -10%), rgba(42, 91, 255, 0.18), transparent 68%);
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease-out);
  pointer-events: none;
}
/* no lift on hover: the 2px rise moved the +/x toggle with the card, which read as the
   icon jumping. hover feedback is now colour and glow only, nothing shifts position. */
.home-faq-entry:hover {
  border-color: var(--card-border-hover);
}
.home-faq-entry:hover::before,
.home-faq-entry[open]::before { opacity: 1; }
.home-faq-entry[open] {
  border-color: rgba(130, 165, 255, 0.5);
  box-shadow: var(--card-shadow), 0 0 42px -22px rgba(42, 91, 255, 0.55);
}
.home-faq-entry summary {
  position: relative;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 12px 20px;
  font-size: 1rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.4;
  color: #e7eeff;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.home-faq-entry summary::-webkit-details-marker { display: none; }
/* the question must be ONE flex item - otherwise inline <span translate="no"> children
   become separate flex items and the summary's gap pushes the words apart */
.home-faq-entry summary .faq-q { flex: 1 1 auto; min-width: 0; }
/* circular +/x toggle on the left (the + rotates 45deg into an x when open) */
.home-faq-entry summary::before {
  content: '';
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 1px solid var(--glass-border);
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a8c0ff' stroke-width='1.7' stroke-linecap='round'%3E%3Cpath d='M12 5v14'/%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E") center / 17px no-repeat,
    linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.015));
  transition:
    transform var(--dur-2) var(--ease-out),
    border-color var(--dur-2) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out);
}
.home-faq-entry[open] summary::before {
  transform: rotate(45deg);
  border-color: rgba(130, 165, 255, 0.7);
  box-shadow: 0 0 18px -4px rgba(42, 91, 255, 0.6), inset 0 0 0 1px rgba(42, 91, 255, 0.2);
}
/* the toggle no longer swells on hover either. its only transform is the 45deg turn
   from + to x when the entry opens, which is state, not a hover effect. */
.home-faq-entry summary:focus-visible {
  outline: 2px solid rgba(130, 165, 255, 0.55);
  outline-offset: -3px;
  border-radius: inherit;
}
/* An entry with no category must not render the chip at all. attr() on a missing
   attribute resolves to an empty string, not to nothing, so the chip still drew
   its border and padding and left an empty box floating at the right edge of the
   row. Pages whose FAQ is one single topic (the /services/ one is all about the
   day of the session) have nothing to put there and should show nothing. */
.home-faq-entry summary:not([data-cat])::after { display: none; }

/* category chip on the right (label comes from data-cat on the summary) */
.home-faq-entry summary::after {
  content: attr(data-cat);
  flex: 0 0 auto;
  margin-left: auto;
  padding: 5px 11px;
  border-radius: 999px;
  border: 1px solid rgba(120, 150, 235, 0.28);
  background: rgba(42, 91, 255, 0.06);
  color: #9fb4ee;
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: border-color var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
}
.home-faq-entry[open] summary::after {
  border-color: rgba(130, 165, 255, 0.55);
  color: #cdddff;
}
.home-faq-answer {
  padding: 0 24px 18px calc(20px + 38px + 16px); /* align under the question, clear of the toggle */
  font-size: 0.93rem;
  line-height: 1.62;
  color: var(--text-mid);
}
.home-faq-answer p + p,
.home-faq-answer p + ul,
.home-faq-answer ul + p { margin-top: 10px; }
.home-faq-answer ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 7px;
}
/* `ul > li`, not `> ul > li`. Every answer wraps its content in .faq-inner, so
   the child combinator never matched a single list on the site: the rows got
   neither the indent nor the anchor, while the bullet below was drawn anyway. */
.home-faq-answer ul > li {
  position: relative;
  padding-left: 20px;
}
/* A list that draws its own markers opts out. Two markers per row is one too
   many, and the built-in bullet would sit under the custom one. */
.home-faq-answer ul[data-no-bullets] > li { padding-left: 0; }
/* The bullet is absolutely positioned, so it may only be drawn on rows the rule
   above actually anchored. It used to match every li at any depth while the
   positioning matched none of them, so every bullet fell back to the
   .home-faq-entry box and the whole list stacked into a single dot in its top
   left corner. Bullet and anchor now share one selector and cannot drift apart. */
.home-faq-answer ul:not([data-no-bullets]) > li::before {
  content: '';
  position: absolute;
  left: 2px;
  top: 0.62em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(42, 91, 255, 0.75);
  box-shadow: 0 0 6px rgba(42, 91, 255, 0.5);
}
.home-faq-answer ul ul { margin-top: 7px; }
.home-faq-answer a {
  color: #a8c0ff;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.home-faq-answer a:hover { color: #d4e2ff; }
@media (max-width: 560px) {
  .home-faq-entry summary {
    gap: 12px;
    padding: 10px 14px;
    font-size: 0.95rem;
  }
  .home-faq-entry summary::before { width: 34px; height: 34px; background-size: 16px; }
  .home-faq-entry summary::after { display: none; }        /* chip is a desktop enhancement */
  .home-faq-answer { padding: 0 15px 15px calc(14px + 34px + 12px); }
}



/* ---------- Screen-reader-only (moved from css/landing.css) ---------- */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- BorderBeam (moved from css/landing.css) ----------
   A light dot travelling a rounded-rect outline. js/landing.js borderBeam()
   measures each [data-border-beam] and writes --beam-path; it was already
   page-agnostic, only this CSS was locked to .page-home. */
/* BorderBeam: a light dot travelling the frame outline. The layer is masked to a thin
   ring (border-box minus content-box) so the glow only shows on the border. */
.border-beam {
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  padding: 2.2px;
  pointer-events: none;
  overflow: hidden;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}
.border-beam-dot {
  position: absolute;
  top: 0;
  left: 0;
  /* round, symmetric glow (NO offset-rotate): a rotating streak spiked its angular
     speed on the short corner arcs (slow-then-jerk); a radial blob has no orientation,
     so it glides through the rounded corners at a perfectly constant speed. width sets
     how LONG the lit stretch reads on the border. */
  width: 620px;
  height: 620px;
  offset-path: var(--beam-path);
  offset-distance: 0%;
  background: radial-gradient(circle at center, rgba(198, 216, 255, 1) 0%, rgba(140, 172, 255, 0.6) 22%, rgba(120, 155, 255, 0.22) 40%, transparent 60%);
  animation: border-beam-move 9s linear infinite;
}
@media (max-width: 640px) {
  /* smaller frame -> shorter perimeter: scale the glow down so it stays a travelling
     beam (not a fully-lit border), but keep it clearly visible via a thicker ring. */
  .border-beam { padding: 2.6px; }
  .border-beam-dot { width: 320px; height: 320px; }
}
@keyframes border-beam-move { to { offset-distance: 100%; } }
/* the featured plan card is far smaller than the hero frame - scale the travelling
   light down so it reads as a moving beam, not a permanently lit edge. */
:is(.plan-card, .u-plan, .s-plan) .border-beam-dot {
  width: 460px;
  height: 460px;
  animation-duration: 7s;
  /* the popular card already has a bright royal border, so a royal beam disappears into
     it - a white-hot core is what actually reads as a travelling light here. */
  background: radial-gradient(circle at center, #ffffff 0%, rgba(224, 237, 255, 0.92) 13%, rgba(165, 196, 255, 0.45) 32%, transparent 62%);
}
:is(.plan-card, .u-plan, .s-plan) .border-beam {
  /* straddle the card's border line: with a 3px ring, pulling the layer 1.5px out puts
     half the beam outside the edge and half inside, i.e. centred ON the line. */
  inset: -1.5px;
  border-radius: calc(var(--r-card-lg) + 1.5px);
  padding: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .border-beam-dot { animation: none !important; opacity: 0.5; }
}

/* ---------- Billing-period switch (moved from css/landing.css) ----------
   Royal segmented control. js/landing.js pricingSwitch() measures the active
   .plan-tab and drives [data-switch-thumb]; that script was already page
   agnostic, only this skin was locked to .page-home. */
.plan-switch-wrap { display: flex; justify-content: center; margin-bottom: clamp(28px, 4vw, 44px); }
.plan-switch {
  position: relative;
  display: inline-flex;
  gap: 4px;
  padding: 5px;
  max-width: 100%;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-mid);
  background: linear-gradient(180deg, rgba(16, 22, 44, 0.55), rgba(10, 14, 30, 0.62));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.plan-tab {
  position: relative;
  z-index: 1;                        /* above the sliding thumb */
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 16px;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: 0.76rem;
  font-weight: 500;
  letter-spacing: 0.03em;
  color: var(--text-low);
  white-space: nowrap;
  cursor: pointer;
  transition:
    color var(--dur-2) var(--ease-out),
    border-color var(--dur-2) var(--ease-out),
    background var(--dur-2) var(--ease-out);
}
.plan-tab:not(.is-active):hover { color: var(--text-mid); }
/* the active pill itself is just text now - the sliding thumb carries the fill, so it
   can travel between options instead of snapping (js/landing.js positions it). */
.plan-tab.is-active { color: #eaf1ff; }
.plan-switch-thumb {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  width: 0;
  height: 0;
  opacity: 0;
  pointer-events: none;
  border-radius: var(--r-pill);
  border: 1px solid rgba(120, 155, 255, 0.42);
  background:
    radial-gradient(130% 150% at 0% 0%, rgba(90, 130, 255, 0.26), transparent 60%),
    linear-gradient(180deg, rgba(42, 91, 255, 0.42), rgba(30, 58, 174, 0.38));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.18),
    0 8px 22px -14px rgba(42, 91, 255, 0.6);
  /* slight overshoot = the spring feel of the reference, without a physics lib */
  transition:
    transform 0.42s cubic-bezier(0.34, 1.35, 0.5, 1),
    width 0.42s cubic-bezier(0.34, 1.35, 0.5, 1),
    height 0.42s cubic-bezier(0.34, 1.35, 0.5, 1),
    opacity 0.2s var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .plan-switch-thumb { transition: opacity 0.2s linear; }
}
.plan-save {
  padding: 2px 7px;
  border-radius: var(--r-pill);
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: #bcd0ff;
  background: rgba(42, 91, 255, 0.22);
  border: 1px solid rgba(120, 160, 255, 0.3);
}
.plan-tab.is-active .plan-save {
  color: #eef3ff;
  background: rgba(255, 255, 255, 0.16);
  border-color: rgba(255, 255, 255, 0.26);
}

/* Touch targets, decided by input device rather than by width. A 39px tab is
   comfortable with a mouse and too small for a finger, and the two do not line
   up with screen size: a 1440px touchscreen laptop needs the bigger target and
   a 760px desktop window does not. pointer: coarse asks the right question.
   The width query stays as a backstop for the tablet range. */
@media (pointer: coarse) {
  .plan-tab { padding-block: 14px; }
}
@media (max-width: 900px) {
  .plan-tab { padding-block: 14px; }
}

/* Narrow screens: the four periods plus their "Save x%" badges cannot fit one row
   (they overflowed, and flex-wrap left a ragged pill). Lay them out as a tidy 2x2
   grid that fills the available width instead. */
@media (max-width: 640px) {
  .plan-switch-wrap { padding-inline: 2px; }
  .plan-switch {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 5px;
    width: 100%;
    border-radius: 20px;          /* pill -> rounded block now that it is two rows */
  }
  /* Three options in a two-column grid left the third one alone with an empty
     cell beside it. Spanning it across both columns fills the row and, unlike
     forcing three columns, leaves the "Save 37%" badge room to breathe even on
     a 320px phone. */
  .plan-switch > .plan-tab:last-child { grid-column: 1 / -1; }
  .plan-tab {
    justify-content: center;
    gap: 6px;
    /* 14px, not 10: takes the tab to a 44px touch target. At 10 it measured 37,
       and at 13 the Lifetime tab came out 42, because the other two are padded
       out by the "Save 37%" badge and it is not. Size for the tab without one. */
    padding: 14px 6px;
    font-size: 0.72rem;
  }
  .plan-save { padding: 2px 6px; font-size: 0.56rem; }
}
/* very small phones: tighten once more so nothing clips */
@media (max-width: 360px) {
  .plan-tab { gap: 5px; padding: 14px 4px; font-size: 0.67rem; }
  .plan-save { padding: 2px 5px; font-size: 0.53rem; }
}


/* ---------- Announcement bar (moved from css/landing.css) ----------
   Site-wide chrome now, not home content: any page that loads this stylesheet
   and drops the markup above <header> gets the bar. Sits at z-index 40, i.e.
   above a hero pulled under the nav, below the sticky header (50) and the
   mobile menu (80). */
.announce {
  /* the hero is pulled up by -116px to sit under the nav, and being positioned it painted
     over this bar and swallowed clicks on the Discord link. sits above the hero, below the
     sticky header (50) and the mobile menu (80). */
  position: relative;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 6px 10px;
  padding: 9px 16px;
  border-bottom: 1px solid var(--glass-border);
  /* same liquid-glass tone as the nav / cards / footer (translucent: aurora shows through) */
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  font-family: var(--font-mono);
  font-size: 0.74rem;
  letter-spacing: 0.06em;
  color: var(--text-mid);
  text-align: center;
}
.announce svg { color: rgba(150, 185, 255, 0.9); flex-shrink: 0; }
.announce a {
  position: relative;
  color: #a8c0ff;
  text-decoration: underline;
  text-underline-offset: 3px;
}
/* text link is ~19px tall; pad the hit area out to a thumb-sized target. absolute, so
   the announcement bar keeps its exact height.
   -13px, not -12px: at 12 the target measured 19 + 24 = 43px, one pixel under the
   44px minimum. The pseudo-element is out of flow, so the bar height is unchanged. */
.announce a::after {
  content: "";
  position: absolute;
  inset: -13px 0;
}
.announce a:hover { color: #d4e2ff; }


/* ---------- App window frame (moved from css/landing.css) ----------
   The browser-style chrome the home carousel puts round every screenshot:
   glass frame, three window dots, a monospace app label. Shared under the
   neutral name .app-frame so a static figure does not have to pretend to be
   a carousel slide; .tbx-car-* stays valid so the home markup is untouched. */
.tbx-car-frame, .app-frame {
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid rgba(130, 160, 240, 0.24);
  background: linear-gradient(180deg, rgba(16, 22, 42, 0.7), rgba(9, 13, 28, 0.82));
  box-shadow: 0 40px 90px -46px rgba(0, 0, 0, 0.85);
}
.tbx-car-bar, .app-frame-bar {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 9px 13px;
  border-bottom: 1px solid rgba(150, 180, 255, 0.12);
  background: rgba(255, 255, 255, 0.02);
}
.tbx-car-bar span, .app-frame-bar span { width: 9px; height: 9px; border-radius: 50%; background: rgba(150, 170, 220, 0.28); }
.tbx-car-bar span:nth-child(1), .app-frame-bar span:nth-child(1) { background: rgba(255, 95, 86, 0.5); }
.tbx-car-bar span:nth-child(2), .app-frame-bar span:nth-child(2) { background: rgba(255, 189, 46, 0.5); }
.tbx-car-bar span:nth-child(3), .app-frame-bar span:nth-child(3) { background: rgba(39, 201, 63, 0.45); }
.tbx-car-bar em, .app-frame-bar em { margin-left: 8px; font-style: normal; font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.03em; color: #8ea0c8; }
/* matches the real app-window ratio of the screenshots, so nothing is cropped. the old
   1092/693 box cut ~10% off the right edge, which sliced the Restore buttons in Backups
   and the third card column in General. */
.tbx-car-frame img, .app-frame img { display: block; width: 100%; aspect-ratio: 1100 / 626; object-fit: cover; object-position: top left; }


/* ---------- Framed product shot (moved from css/landing.css) ----------
   The glass frame the home hero puts round its screenshot. Shared as .shot-frame
   so a static figure can use it without pretending to be the hero; .hero-shot-*
   stays valid so the home markup is untouched. The 3D bits belong to the tilt,
   not the frame, so they now hang off [data-hero-tilt] (js/landing.js heroTilt)
   instead of being paid for by every page that just wants the border. */
.hero-shot-frame,
.shot-frame {
  position: relative;
  max-width: 1040px;
  margin: 0 auto;
  padding: 10px;
  border-radius: 26px;
  border: 1px solid rgba(120, 150, 255, 0.16);
  background: linear-gradient(180deg, rgba(18, 24, 46, 0.9), rgba(9, 12, 26, 0.92));
}
[data-hero-tilt] {
  transform-style: preserve-3d;
  will-change: transform;
}
.hero-shot-inner,
.shot-inner {
  position: relative;
  border-radius: 17px;
  overflow: hidden;
  background: #0a0e1c;
}
.hero-shot,
.shot-inner img { display: block; width: 100%; height: auto; }

/* ---------- Spec pills (moved from css/landing.css) ----------
   Vanilla port of the originui hero pill: glass pill, leading icon divided from
   the label by a hairline, calm hover. Shared as .spec-pills so any page can use
   the row; .hero-stats stays valid so the home markup is untouched.

   No entrance is baked in here on purpose. A row in a hero enters on load and a
   row further down enters on scroll, and that is a property of WHERE the row
   sits, not of what it looks like. The page says which by tagging the element
   .hero-item or .reveal-item; this rule only draws the pill. */
/* The pills carry a backdrop-filter, and in a hero they also animate in. A blur
   on a MOVING element has to re-sample and re-blur whatever is behind it on
   every frame - and on the home page what is behind it is a live shader canvas
   that is repainting anyway. Blur + travel + an animating backdrop is the most
   expensive combination on the page, and it lands inside the entrance, which is
   what made the reveal look like it was stuttering.

   So they arrive without travelling: opacity only, no transform, which is what
   .reveal-fade already means elsewhere in this file. The blur result stays
   valid for the whole animation instead of being thrown away 60 times a second,
   and at rest the row is identical.

   Two selectors because the two markup shapes differ: the home page marks each
   <li> as the item, every other hero marks the <ul>. */
.hero-seq .hero-stats.hero-item,
.hero-seq .spec-pills.hero-item,
.hero-seq .hero-stats li.hero-item,
.hero-seq .spec-pills li.hero-item {
  --reveal-rise: 0px;
}

.hero-stats,
.spec-pills {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: 32px;
  padding: 0;
  list-style: none;
}
.hero-stats li,
.spec-pills li {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 7px 14px;
  border-radius: var(--r-pill);
  border: 1px solid rgba(150, 180, 255, 0.12);
  background: linear-gradient(180deg, rgba(30, 38, 66, 0.28), rgba(14, 18, 38, 0.32));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  backdrop-filter: blur(14px) saturate(150%);
  font-family: var(--font-mono);
  font-size: 0.73rem;
  letter-spacing: 0.05em;
  color: rgba(198, 210, 238, 0.55);
  white-space: nowrap;
  transition:
    border-color var(--dur-1) var(--ease-out),
    color var(--dur-1) var(--ease-out);
}
.hero-stats li:hover,
.spec-pills li:hover {
  border-color: rgba(150, 180, 255, 0.28);
  color: rgba(212, 222, 245, 0.8);
}
/* leading icon, divided from the label by a faint hairline (kept quiet so the row
   stays a supporting spec strip, not a second button group under the CTAs) */
.hero-stats li svg,
.spec-pills li svg {
  width: 21px;
  height: 21px;
  color: rgba(150, 180, 255, 0.62);
  flex-shrink: 0;
  padding-right: 9px;
  border-right: 1px solid rgba(255, 255, 255, 0.08);
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* Honeypot, shared. Lives here and not in a page stylesheet because losing
   it renders the trap field as a real input: people type in it and the API
   drops them as bots, silently. A rule that costs signups when it goes missing
   does not belong in a layer a page can choose not to load. */
.wl-hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }

/* ── Toasts ───────────────────────────────────────────────────────────────
   Outcome messages for the waitlist. Anatomy is the familiar one (icon,
   title + description, dismiss), but built from this site's own glass rather
   than generic notification chrome, so it reads as part of the product.

   "Success" is royal, not green: the palette here has no green, and borrowing
   one would be the only foreign colour on the page. */
.pt-toasts {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: min(384px, calc(100vw - 40px));
  pointer-events: none;               /* the stack never blocks the page */
}

.pt-toast {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 12px 15px 15px;
  border-radius: var(--r-card, 16px);
  border: 1px solid var(--border-mid, rgba(150, 180, 255, 0.18));
  background: rgba(10, 14, 30, 0.86);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
          backdrop-filter: blur(16px) saturate(140%);
  box-shadow: 0 18px 44px -18px rgba(0, 0, 0, 0.85);
  overflow: hidden;
  pointer-events: auto;
  animation: pt-toast-in 0.46s cubic-bezier(0.33, 1, 0.68, 1) both;
}

.pt-toast-icon {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid rgba(150, 180, 255, 0.26);
  background: rgba(42, 91, 255, 0.2);
  color: #cfe0ff;
}
.pt-toast.is-error .pt-toast-icon {
  border-color: rgba(255, 145, 145, 0.32);
  background: rgba(255, 90, 90, 0.15);
  color: #ffb4b4;
}

.pt-toast-body { flex: 1; min-width: 0; }
.pt-toast-title {
  margin: 0;
  font-size: 0.92rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.35;
  color: var(--text-hi, #eaf0ff);
}
.pt-toast-desc {
  margin: 3px 0 0;
  font-size: 0.84rem;
  line-height: 1.55;
  color: var(--text-mid, #b8c4e0);
}

.pt-toast-close {
  position: relative;
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-low, rgba(214, 225, 245, 0.6));
  cursor: pointer;
  opacity: 0.65;
  transition: opacity 0.18s ease, background-color 0.18s ease;
}
.pt-toast-close:hover { opacity: 1; background: rgba(150, 180, 255, 0.1); }
.pt-toast-close:focus-visible {
  opacity: 1;
  outline: 2px solid rgba(94, 140, 255, 0.85);
  outline-offset: 2px;
}

/* The one flourish: a royal hairline draining left to right, so the toast says
   it will leave on its own instead of just vanishing. */
.pt-toast-timer {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 100%;
  transform-origin: left center;
  background: linear-gradient(90deg, rgba(42, 91, 255, 0), #2a5bff);
  animation: pt-toast-timer 6.5s linear forwards;
}
.pt-toast.is-error .pt-toast-timer {
  background: linear-gradient(90deg, rgba(255, 120, 120, 0), #ff7676);
}

/* Leaving is split across two elements on purpose. Collapsing height and
   padding on the card itself looked broken: the stack is pinned to the bottom
   of the screen, so the shrinking box crept downward while the disappearing
   padding pushed the text upward inside it, and the two slid apart.
   Now the card only moves and fades, keeping its shape, while the slot around
   it (which has no padding of its own) closes the space. */
.pt-toast-slot { overflow: hidden; }

.pt-toast-slot.is-leaving {
  height: 0 !important;
  margin-bottom: -10px;            /* swallows the flex gap as the slot closes */
  transition:
    height 0.28s cubic-bezier(0.4, 0, 0.2, 1) 0.08s,
    margin-bottom 0.28s cubic-bezier(0.4, 0, 0.2, 1) 0.08s;
}

.pt-toast.is-leaving {
  opacity: 0;
  transform: translate3d(30px, 0, 0) scale(0.97);
  pointer-events: none;
  transition:
    opacity 0.18s ease-out,
    transform 0.26s cubic-bezier(0.4, 0.02, 1, 0.7);
}

/* Arrives from the right and settles just past its resting place before coming
   back, so it reads as something that flew in and stopped rather than a box
   that faded up. */
@keyframes pt-toast-in {
  0%   { opacity: 0; transform: translate3d(34px, 8px, 0) scale(0.94); }
  55%  { opacity: 1; transform: translate3d(-4px, 0, 0) scale(1.008); }
  78%  { transform: translate3d(1px, 0, 0) scale(0.998); }
  100% { opacity: 1; transform: none; }
}
@keyframes pt-toast-timer {
  from { transform: scaleX(1); }
  to   { transform: scaleX(0); }
}

/* Phones: span the width, and stay clear of the notch and the home indicator.
   env() falls back to 0 where it is unsupported, so max() keeps the old inset. */
@media (max-width: 560px) {
  .pt-toasts {
    right: max(12px, env(safe-area-inset-right));
    left: max(12px, env(safe-area-inset-left));
    bottom: max(12px, env(safe-area-inset-bottom));
    width: auto;
    gap: 8px;
  }
  .pt-toast { padding: 13px 10px 14px 13px; border-radius: 14px; }
  .pt-toast-title { font-size: 0.9rem; }
  .pt-toast-desc { font-size: 0.82rem; }
}

/* Short screens, mostly phones held sideways: there is barely any height, so the
   toast tightens up rather than eating a third of the view. */
@media (max-height: 460px) {
  .pt-toast { padding: 10px 10px 11px 12px; gap: 10px; }
  .pt-toast-icon { width: 24px; height: 24px; }
  .pt-toast-desc { margin-top: 2px; font-size: 0.8rem; }
  .pt-toasts { gap: 8px; }
}

@media (prefers-reduced-motion: reduce) {
  .pt-toast { animation: none; }
  /* The space still closes, so the stack stays tidy. Only the travel goes. */
  .pt-toast.is-leaving { transform: none; transition: opacity 0.12s linear; }
  .pt-toast-slot.is-leaving {
    transition: height 0.12s linear, margin-bottom 0.12s linear;
  }
  .pt-toast-timer { animation: none; transform: scaleX(0); }
}

/* The X stays visually small, but on a touch screen it needs a finger-sized
   target. Grown with a pseudo-element so nothing about the look changes. */
@media (pointer: coarse) {
  .pt-toast-close::after { content: ''; position: absolute; inset: -9px; }
}

/* ── Page hero: bars, pill, split headline ─────────────────────────────────
   Promoted out of utility.css when /services/ needed the same hero. Two pages
   now render it, so it belongs in the layer neither of them owns: left where it
   was, the second page would have had to copy it, and the two copies would drift
   the first time one of them was touched. Page-specific spacing stays local. */
/* These two travel WITH the hero, not with the page that first used it.
   Left behind in utility.css they were undefined on every other page, which
   made the padding below an invalid calc(); the browser then dropped the whole
   shorthand while the negative margin-top survived, and the hero slid up under
   the floating nav. A rule and the tokens it reads are one unit to move. */
:root {
  --hero-lift: 64px;
  --hero-tail: 19px;
}

.u-hero {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  /* No min-height. It used to be a viewport-relative clamp, and because the hero
     also centres its content, any height the clamp added beyond the content was
     split in half and half of it landed UNDER the social row. That made the gap
     below the icons depend on window height: 31px at 1440x900, 69px at 760x900.
     Content-driven height means the tail is always exactly
     padding-bottom - --hero-lift, at every size. The hero still comes out
     550-690px tall, which is a hero, and the product shot keeps its slice above
     the fold instead of being pushed down by empty space. */
  margin-top: calc(var(--nav-h) * -1 - 14px);   /* sit under the floating nav */
  /* 72px at the bottom, not 60. The hero centres its content inside a fixed
     min-height, so anything that makes the content taller (the waitlist status
     message appearing after a submit) grows it DOWNWARDS, towards a showcase
     that is simultaneously pulled upwards. This is the floor of clearance that
     keeps the social row off the screenshot. */
  /* Only the last term is air you can see. The hero is pulled up under the
     floating nav, so the first two terms (64 + 14) are spent covering the nav
     and its top offset before a single pixel shows. At the old "+40" that left
     26px of visible breathing room and the badge sat jammed against the nav. */
  padding: calc(var(--nav-h) + 14px + 64px) 0 calc(var(--hero-lift) + var(--hero-tail));
  overflow: hidden;
}

/* --- gradient bars --- */
.u-bars {
  position: absolute;
  inset: 0;
  z-index: -1;
  display: flex;
  overflow: hidden;
  pointer-events: none;
  /* Fade BOTH ends. The reference section is full-viewport, so its bars end at
     the fold and the cut is never seen. This hero ends mid-page, so at full
     strength the bars were sliced flat by overflow:hidden and read as a hard
     rule across the screen. Top fade keeps them off the headline; bottom fade
     dissolves them into the page instead of stopping dead. */
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 42%, #000 74%, transparent 99%);
          mask-image: linear-gradient(180deg, transparent 0%, #000 42%, #000 74%, transparent 99%);
}
.u-bars span {
  flex: 1 0 calc(100% / 15);
  max-width: calc(100% / 15);
  height: 100%;
  transform-origin: bottom;
  transform: scaleY(calc(var(--h) / 100));
  /* 0.72, not 0.62: each bar's gradient peaks at its own base, which is exactly
     where the bottom mask now removes it. Raising the source recovers the
     presence the fade costs, without bringing back the hard edge. */
  background: linear-gradient(to top, rgba(42, 91, 255, 0.72), transparent 78%);
  animation: u-bar-pulse 2s var(--ease-out) infinite alternate;
  animation-delay: var(--d);
}
@keyframes u-bar-pulse {
  from { transform: scaleY(calc(var(--h) / 100)); opacity: 0.72; }
  to   { transform: scaleY(calc(var(--h) / 100 * 1.06)); opacity: 1; }
}

.u-hero-inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  max-width: 860px;
  margin-inline: auto;
}

/* badge in the reference's social-proof slot: an overlapping stack plus a count.
   The stack is game art, not faces. There are real customer avatars in
   /img/customers/, but those five people vouched for the REMOTE SERVICE, and
   the brief forbids attributing service testimonials to the app. Game logos
   claim nothing about who anyone is. */
.u-hero-pill {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 6px 16px 6px 8px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-mid);
  background: rgba(10, 14, 30, 0.6);
  -webkit-backdrop-filter: blur(8px);
          backdrop-filter: blur(8px);
  font-size: 0.85rem;
  color: var(--text-mid);
}
.u-pill-text strong { color: var(--text-hi); font-weight: 600; }
.u-avatars {
  display: inline-flex;
  align-items: center;
}
/* Port of the UserAvatars maths, done in CSS so it needs no JS:
     overlap 60%  -> each bubble pulls back 0.4 x its own size
     focusScale 1.2, so the hovered bubble grows and the ones after it slide
     right by (size x (1 + focusScale) / 2) - (size x overlap) = 15px at 30px. */
.u-av {
  --av: 30px;
  position: relative;
  width: var(--av);
  height: var(--av);
  border-radius: 50%;
  overflow: hidden;
  border: 1.5px solid rgba(150, 180, 255, 0.28);
  background: #0a0e1c;
  box-shadow: 0 3px 10px -4px rgba(0, 0, 0, 0.8);
  transition: transform 0.28s cubic-bezier(0.34, 1.35, 0.5, 1),
              border-color var(--dur-1) var(--ease-out);
}
.u-av + .u-av { margin-left: calc(var(--av) * -0.4); }
.u-av img { width: 100%; height: 100%; object-fit: cover; display: block; }
.u-avatars:hover .u-av:hover {
  transform: scale(1.2);
  border-color: rgba(150, 180, 255, 0.55);
  z-index: 2;
}
.u-avatars:hover .u-av:hover ~ .u-av { transform: translateX(15px); }

@media (prefers-reduced-motion: reduce) {
  .u-av { transition: none; }
  .u-avatars:hover .u-av:hover { transform: none; }
  .u-avatars:hover .u-av:hover ~ .u-av { transform: none; }
}
@media (max-width: 480px) {
  .u-av { --av: 26px; }
  .u-hero-pill { font-size: 0.8rem; gap: 10px; }
}

/* Internal rhythm tightened by ~28px in total (26/22/30/30 -> 20/18/26/24).
   Nothing here is cramped at these values, and the height it gives back is what
   lets the screenshot show without shrinking the type. */
.u-hero h1 {
  margin: 20px 0 0;
  line-height: 1.08;
  letter-spacing: -0.03em;
  color: var(--text-hi);
}
.u-h1-plain,
.u-h1-serif {
  display: block;
  font-size: clamp(1.9rem, 5.6vw, 3.5rem);
}
.u-h1-plain { font-weight: 500; }
/* the site's existing <em> motif, promoted to a whole headline line */
.u-h1-serif {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  letter-spacing: -0.012em;
  background: linear-gradient(115deg, #d4e2ff 8%, #7fa2ff 54%, var(--accent) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

.u-hero-lede {
  margin-top: 18px;
  font-size: clamp(1rem, 2.2vw, 1.18rem);
  line-height: 1.6;
  color: var(--text-mid);
}

/* ── App gallery: starfield section + coverflow carousel ───────────────────
   Moved out of landing.css when /services/ needed the same gallery for the
   Toolbox. De-scoped rather than given a second page name: a component two
   pages render is a shared component, and a scope list is a thing to forget to
   update on the third. Driven by js/landing.js tbxCarousel. */
/* ============================ APP GALLERY STARFIELD ============================
   A subtle starfield behind the app gallery (vanilla port of 21st.dev Sparkles).
   js/sparkles.js draws the twinkling dots on <canvas data-sparkles>. The gallery sits
   centred in it via symmetric padding, and the stars fade evenly at top + bottom. */
.tbx {
  position: relative;
  overflow: hidden;
  /* small margin + symmetric padding: the gallery sits centred in the starfield, and the
     margin + padding together keep the gap to the neighbours on the ~116px rhythm */
  margin-top: clamp(24px, 2.4vw, 40px);
  padding-top: clamp(60px, 7vh, 96px);
  padding-bottom: clamp(60px, 7vh, 96px);
}
.tbx + .section { margin-top: clamp(24px, 2.4vw, 40px); }
.tbx > .container { position: relative; z-index: 1; }

/* ============================ APP GALLERY (carousel) ============================
   Coverflow carousel touring real screenshots from both Prospira apps — the Toolbox
   and the Tweaking Utility. Prev/next arrows, tick pagination + counter, and a caption
   (badge + title + description) that swaps per slide. Driven by js/landing.js tbxCarousel. */
.tbx-car { position: relative; margin-top: 28px; }
.tbx-car-row { position: relative; }
.tbx-car-stage {
  position: relative;
  overflow: hidden;
  padding: 14px 0 8px;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 14%, #000 86%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 14%, #000 86%, transparent 100%);
}
.tbx-car-track {
  display: flex;
  gap: 30px;
  transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1);
}
.tbx-car-slide {
  flex: 0 0 66%;
  opacity: 0.32;
  transform: scale(0.9);
  transition: opacity 0.5s var(--ease-out), transform 0.5s var(--ease-out);
}
.tbx-car-slide.is-active { opacity: 1; transform: scale(1); }

/* browser-style glass frame around each screenshot */
/* MOVED to css/prospira.css and de-scoped as .tbx-car-frame / .app-frame.
   Only the carousel-specific active state stays here. */
.tbx-car-slide.is-active .tbx-car-frame {
  border-color: rgba(120, 160, 255, 0.4);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 46px 100px -44px rgba(0, 0, 0, 0.9);
}

/* arrows */
.tbx-car-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  cursor: pointer;
  color: var(--text-hi);
  border: 1px solid var(--border-mid);
  background: linear-gradient(180deg, rgba(18, 24, 46, 0.72), rgba(10, 14, 30, 0.8));
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.7);
  transition: border-color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out), opacity var(--dur-1) var(--ease-out);
}
.tbx-car-nav:hover { border-color: rgba(120, 160, 255, 0.5); }
.tbx-car-nav:active { transform: translateY(-50%) scale(0.94); }
.tbx-car-nav[disabled] { opacity: 0.3; cursor: default; }
.tbx-car-nav svg { width: 22px; height: 22px; }
.tbx-car-prev { left: -8px; }
.tbx-car-next { right: -8px; }

/* caption (swaps per slide) */
.tbx-car-cap { text-align: center; max-width: 620px; margin: 18px auto 0; min-height: 74px; }
.tbx-car-cap h3 { font-size: 1.4rem; font-weight: 600; letter-spacing: -0.01em; color: var(--text-hi); margin: 0 0 8px; }
.tbx-car-cap p { font-size: 0.98rem; line-height: 1.6; color: #a2b0d4; }

/* footer: ticks + counter */
.tbx-car-foot { display: flex; align-items: center; justify-content: center; gap: 18px; margin-top: 16px; }
.tbx-car-dots { display: flex; align-items: center; gap: 7px; }
/* The visible dot is a 4px bar by design and a 4px bar cannot be hit with a
   finger. The bar keeps its size and an invisible box takes the tap instead.
   Height only, plus half the 7px gap each side: a wider box would overlap its
   neighbour and hand the tap to the wrong slide. */
.tbx-car-dot::after {
  content: "";
  position: absolute;
  left: -3px;
  right: -3px;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
.tbx-car-dot {
  position: relative;
  width: 20px;
  height: 4px;
  border-radius: 3px;
  cursor: pointer;
  border: 0;
  padding: 0;
  background: rgba(150, 175, 235, 0.24);
  transition: background-color var(--dur-1) var(--ease-out), width var(--dur-1) var(--ease-out);
}
/* the dash is 4px tall, far under the ~44px touch minimum. an invisible pad grows the
   hit area without touching the look; it reaches exactly half the 7px gap on each side,
   so neighbouring targets meet but never overlap (an overlap would swallow taps meant
   for the dot next door). */
.tbx-car-dot::after {
  content: "";
  position: absolute;
  top: 50%;
  left: -3.5px;
  right: -3.5px;
  height: 44px;
  transform: translateY(-50%);
}
.tbx-car-dot:hover { background: rgba(150, 175, 235, 0.44); }
.tbx-car-dot.is-active { width: 30px; background: var(--accent); }
.tbx-car-count { font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.08em; color: #8ea0c8; }
.tbx-car-count b { color: #cfe0ff; font-weight: 600; }

@media (max-width: 760px) {
  .tbx-car-slide { flex-basis: 84%; }
  .tbx-car-track { gap: 16px; }
  .tbx-car-nav { width: 42px; height: 42px; }
  .tbx-car-prev { left: 2px; }
  .tbx-car-next { right: 2px; }
  .tbx-car-cap { min-height: 98px; }
}
@media (prefers-reduced-motion: reduce) {
  .tbx-car-track,
  .tbx-car-slide { transition: none; }
}


/* ---------- Dropdown (.s-sel) -----------------------------------------------
   Promoted from css/services.css, where it was the booking dialog's country
   picker. It is a listbox, not a native <select>, because a native popup cannot
   be styled to match a dark dialog on any platform - and once one control on the
   site behaved like this, a second one that behaved differently would be the
   inconsistency. Driven by js/select.js.

   The keyboard contract is the native one: arrows move, Home/End jump, Enter or
   Space commits, Escape closes without changing anything, Tab closes and moves
   on. Focus never leaves the trigger; the highlighted row is announced through
   aria-activedescendant, which is what keeps a focus trap around it working -
   a trap only knows how to cycle real focusable elements. */
.s-sel { position: relative; }
.s-sel-btn {
  width: 100%;
  min-height: 44px;
  padding: 11px 13px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  border-radius: 12px;
  border: 1px solid var(--border-mid);
  background: linear-gradient(180deg, rgba(10, 14, 30, 0.6), rgba(7, 10, 22, 0.7));
  color: var(--text-hi);
  font-family: inherit;
  font-size: 0.92rem;
  text-align: left;
  cursor: pointer;
}
/* Placeholder weight until something is chosen, matching ::placeholder. */
.s-sel-value.is-empty { color: var(--text-low); }
.s-sel-chev {
  flex-shrink: 0;
  color: #8ea3cc;
  transition: transform var(--dur-1) var(--ease-out);
}
/* SHAPE stays on .s-sel-btn; STATE hangs off [data-sel-btn], the attribute
   js/select.js already requires on every trigger. The language switcher's
   trigger is a nav icon button, so it deliberately does not take the field
   shape - and used to miss the state with it: with its list open it looked
   exactly as it did closed, and announced focus through the global outline
   instead of this glow. Same control, two behaviours, on two pages. */
[data-sel-btn][aria-expanded="true"] .s-sel-chev { transform: rotate(180deg); }
.s-sel-btn:hover { border-color: var(--border-strong); }
[data-sel-btn]:focus-visible,
[data-sel-btn][aria-expanded="true"] {
  outline: none;
  border-color: rgba(120, 155, 255, 0.5);
  box-shadow: 0 0 0 3px rgba(42, 91, 255, 0.16);
}

.s-sel-list {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 6px);
  /* --z-menu, not a bare 5: this file defines --z-nav/--z-menu/--z-overlay and
     four other popups use the token. The language switcher had to override this
     to sit above the nav, which is what an under-specified default buys. */
  z-index: var(--z-menu);
  margin: 0;
  padding: 6px;
  list-style: none;
  /* A grid with a gap, not a flush stack: each highlight needs unlit space
     around it or dragging the pointer down the list reads as one growing block
     instead of a row at a time. */
  display: grid;
  gap: 3px;
  max-height: 268px;
  overflow-y: auto;
  border-radius: 12px;
  border: 1px solid rgba(120, 160, 255, 0.28);
  /* Opaque enough to read against whatever it covers - a translucent popup over
     translucent inputs turns both to mush. */
  background: linear-gradient(180deg, rgba(16, 22, 46, 0.98), rgba(9, 13, 28, 0.99));
  box-shadow: 0 26px 50px -24px rgba(0, 0, 0, 0.9);
  animation: s-sel-in 0.14s var(--ease-out) both;
}
.s-sel-list[hidden] { display: none; }
@keyframes s-sel-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .s-sel-list { animation: none; }
  .s-sel-chev { transition: none; }
}

/* Shorter than an input on purpose. The trigger stays 44px to line up with the
   fields around it, but a row in an open list is a target you are already
   pointing at, not one you have to find, so it does not need the same height. */
.s-sel-opt {
  padding: 7px 11px;
  border-radius: 8px;
  font-size: 0.9rem;
  line-height: 1.35;
  color: var(--text-mid);
  cursor: pointer;
}
/* On touch there is no pointer to aim with, so the rows go back up to a
   comfortable tap target. */
@media (pointer: coarse) {
  .s-sel-opt { padding: 10px 11px; }
}
/* One highlight for both the pointer and the keyboard, so the two never
   disagree about which row is about to be chosen. */
.s-sel-opt:hover,
.s-sel-opt.is-active {
  background: rgba(120, 160, 255, 0.12);
  color: var(--text-hi);
}
.s-sel-opt[aria-selected="true"] {
  color: #eaf0ff;
  background: linear-gradient(180deg, rgba(42, 91, 255, 0.26), rgba(30, 58, 174, 0.22));
}

/* A group heading: a row that labels the ones under it and cannot be chosen.
   Lives here rather than in contact.css, which only /contact/ loads - it carries
   the shared s-sel- prefix, js/select.js already skips it (it queries .s-sel-opt),
   and the country list is the next one that will want grouping. */
.s-sel-group {
  padding: 9px 11px 5px;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-low);
  cursor: default;
}
.s-sel-group:not(:first-child) {
  margin-top: 4px;
  border-top: 1px solid rgba(150, 180, 255, 0.1);
}
/* Phone: one step down, the same as the other fields shrink to. This used to
   live in the booking dialog's own narrow-screen block, which no page but
   /services/ loads. */
@media (max-width: 560px) {
  .s-sel-btn { font-size: 0.9rem; }
}

/* ---------- Storage notice (js/consent.js) ---------------------------------
   Bottom-left, small, and deliberately NOT a modal: no overlay, no dimming, no
   focus trap, nothing blocked. A consent dialogue that holds the page hostage
   is the single most common conversion leak on a site like this, and here it
   would also be dishonest - there is nothing being asked for that would justify
   stopping someone from reading.

   Left rather than right: the cart popover, the language menu and the login
   menu all open from the right side of the nav, and the "back to top" affordance
   lives bottom-right on long pages. The empty corner is the left one. */
.cc {
  position: fixed;
  /* env() keeps it clear of the home indicator and of a landscape notch. max()
     means the inset only ever ADDS to the gap - on a device with no inset the
     env() resolves to 0 and the plain value wins. */
  left: max(16px, env(safe-area-inset-left));
  bottom: max(16px, env(safe-area-inset-bottom));
  z-index: var(--z-notice);
  /* Wider and shallower than a card, with a tighter radius: this is a bar, not a
     panel. One line of copy makes the extra width free, and the shorter box
     covers less of the page while being easier to read at a glance. */
  width: 480px;
  max-width: calc(100vw - 32px);
  /* Symmetric, so top and bottom read the same. It was 15/16 with a 44px link in
     a row of 38px buttons: the link set the row height, the buttons centred
     inside it, and the 3px of slack under them landed on top of the bottom
     padding - measured 16px above the first glyph against 20px below the last
     button. Equal padding plus equal control heights is what actually fixes it. */
  padding: 16px 18px;
  /* The card radius, same as every other surface on the site. Wider and
     shallower than a card, but not a sharper corner - the shape is what changed,
     not the language. */
  border-radius: var(--r-card-lg);
  border: 1px solid var(--card-border);
  background:
    radial-gradient(120% 150% at 0% 0%, rgba(42, 91, 255, 0.14), transparent 58%),
    linear-gradient(180deg, rgba(13, 18, 38, 0.97), rgba(8, 12, 26, 0.98));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 26px 54px -24px rgba(0, 0, 0, 0.8);
  /* No backdrop-filter, deliberately. The fill above is 97-98% opaque, so a
     26px blur showed through in 2-3% of the box - but .page-stars is a fixed
     full-viewport canvas repainting every frame, which dirtied this element's
     backdrop and made the compositor re-run that blur at refresh rate for as
     long as the notice was on screen. All cost, no picture. */
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.28s var(--ease-out), transform 0.28s var(--ease-out);
}
/* The same lit top edge every .card on this site carries. One hairline is what
   makes a floating box look like it belongs to the page rather than to the
   browser. */
.cc::before {
  content: '';
  position: absolute;
  top: 0;
  left: 14%;
  right: 14%;
  height: 1px;
  background: var(--glass-hairline);
  pointer-events: none;
}
.cc.is-in { opacity: 1; transform: none; }
.cc.is-out { opacity: 0; transform: translateY(8px); }

.cc-head { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; }
/* Just the glyph. The plate behind it was one more box inside a box that is
   already a card, and Chanel's rule applies: it was the accessory to remove. */
.cc-badge {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  color: #a8c4ff;
}
.cc-title {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--text-hi);
}
.cc-body {
  margin: 0 0 13px;
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--text-mid);
}

.cc-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
/* Smaller than the site's 44px controls on purpose - this is a dismissable
   notice, not a form. Still well past the 24px minimum a pointer target
   needs, and both buttons stay the same size as each other. */
.cc-btn { min-height: 38px; padding: 8px 16px; font-size: 0.84rem; }
/* A text link, but a real 44px target - it sits beside buttons and is chosen
   just as often. */
.cc-link {
  display: inline-flex;
  align-items: center;
  /* Goes to the far end, so the two real choices stay together and read as a
     pair rather than as three equal options. It lives here rather than in a
     `.cc-acts .cc-link` rule: this button is built in one place and appended to
     .cc-acts on the next line, so the descendant selector bought specificity
     against nothing. */
  margin-left: auto;
  /* The same height as the buttons beside it - it is the tallest thing in the
     row, so anything else silently becomes the row's height. */
  min-height: 38px;
  font: inherit;
  font-size: 0.84rem;
  color: var(--text-low);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.cc-link:hover { color: var(--text-mid); }

/* The inventory. Every key, what it is for, how long it lives - the point is
   that it can be checked against the browser's own storage panel and found to
   be complete. A summary would be easier to write and worth less. */
.cc-detail {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border-soft);
  max-height: 46vh;
  overflow-y: auto;
}
.cc-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 12px; }
/* The list has two halves - what runs the site, and what exists only because
   you said yes. Running them together into one column of keys would hide the
   only distinction in it that a visitor is actually being asked about. */
.cc-group {
  margin: 0 0 9px;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-low);
}
.cc-list + .cc-group { margin-top: 16px; }
.cc-row-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.cc-key {
  font-family: var(--font-mono);
  font-size: 0.74rem;
  color: var(--text-hi);
  word-break: break-all;
}
.cc-where {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-low);
}
.cc-what { margin: 3px 0 0; font-size: 0.82rem; line-height: 1.5; color: var(--text-mid); }
.cc-life { margin: 2px 0 0; font-size: 0.76rem; color: var(--text-low); }
.cc-none {
  margin: 12px 0 0;
  font-size: 0.82rem;
  line-height: 1.55;
  color: var(--text-low);
}

@media (max-width: 560px) {
  .cc {
    left: max(12px, env(safe-area-inset-left));
    right: max(12px, env(safe-area-inset-right));
    bottom: max(12px, env(safe-area-inset-bottom));
    width: auto;
    padding: 15px 16px 16px;
  }
  /* On one line the two buttons would leave the link stranded on its own row, so
     it rejoins them and the pair simply share the width. */
  .cc-link { margin-left: 0; }
  .cc-btn { flex: 1 1 auto; }
  .cc-detail { max-height: 40vh; }
}
/* Only the narrowest phones. Measured across the range: at 320px the row needs
   about 260px of content against 264px of usable width, and the gap between the
   buttons pushes it over - so it wraps whatever the flex rules say. From 360px
   up it fits on one line, and an earlier version of this block was set at 379px,
   which forced a second row onto phones that never needed one and grew the panel
   from 130px to 168px for nothing. Keep this breakpoint measured, not rounded. */
@media (max-width: 344px) {
  .cc-acts { row-gap: 4px; }
  .cc-btn { flex: 1 1 0; min-width: 0; }
  .cc-link { flex: 1 0 100%; min-height: 34px; }
}

@media (prefers-reduced-motion: reduce) {
  .cc { transition: none; }
}


/* ---------- Rozwijane menu nawigacji (.nav-pop) ----------------------------
   Jeden WSPOLNY panel dla wszystkich pozycji, nie osobny na kazda. To jest cala
   sztuczka tego wzorca: przechodzac z "Utility" na "Services" panel nie znika i
   nie pojawia sie od nowa - plynnie zmienia szerokosc, wysokosc i pozycje. We
   framer-motion robi to `layoutId`; tutaj robi to jeden element, ktoremu JS
   nadaje zmierzone wymiary aktywnej zawartosci, plus przejscie CSS.

   Panele siedza w DOM caly czas i sa ukryte przez visibility, NIE display:none -
   element z visibility:hidden nadal ma uklad, wiec da sie go zmierzyc zanim
   zostanie pokazany. Przy display:none wymiary bylyby zerowe i panel skakalby
   przy pierwszym otwarciu.

   Nawigacja desktopowa znika ponizej 1120px (mobilne menu przejmuje), wiec caly
   ten blok dotyczy wylacznie duzych ekranow. */
header[data-nav-floating] nav { position: relative; }

.nav-pop {
  position: absolute;
  /* 8px, nie 14: ta przerwa nie nalezy ani do nawigacji, ani do panelu, wiec
     kursor przejezdzajacy przez nia opuszcza oba. Krotsza droga + opoznienie
     zamkniecia w nav-menu.js sprawiaja, ze da sie zjechac do menu. */
  top: calc(100% + 8px);
  left: 0;
  z-index: 60;
  transform: translateX(var(--pop-x, 0)) translateY(-6px) scale(0.97);
  transform-origin: top center;
  opacity: 0;
  pointer-events: none;
  overflow: hidden;
  border-radius: var(--r-card-lg);
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow);
  transition:
    opacity var(--dur-1) var(--ease-out),
    transform var(--dur-2) var(--ease-out),
    width var(--dur-2) var(--ease-out),
    height var(--dur-2) var(--ease-out);
}
.nav-pop.is-open {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(var(--pop-x, 0)) translateY(0) scale(1);
}
/* Dopoki panel nigdy sie nie otworzyl, nie ma czego animowac - bez tego pierwsze
   otwarcie jechaloby szerokoscia od zera. */
.nav-pop.is-cold { transition: none; }

.nav-pop-inner { position: relative; }
.nav-pop-panel {
  position: absolute;
  top: 0;
  left: 0;
  visibility: hidden;
  opacity: 0;
  padding: 14px;
  transition: opacity var(--dur-1) var(--ease-out);
}
.nav-pop-panel.is-active { visibility: visible; opacity: 1; }

/* --- wariant z obrazkami --- */
.nav-pop-grid { display: grid; grid-template-columns: repeat(2, 260px); gap: 12px; }
.nav-pop-card {
  display: flex;
  gap: 12px;
  padding: 10px;
  border-radius: var(--r-card);
  border: 1px solid transparent;
  transition: background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
}
.nav-pop-card:hover { background: rgba(150, 180, 255, 0.06); border-color: var(--border-mid); }
.nav-pop-card img {
  flex-shrink: 0;
  width: 116px;
  height: 65px;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--border-mid);
}
.nav-pop-card h4 { margin: 0 0 3px; font-size: 0.92rem; font-weight: 600; color: var(--text-hi); letter-spacing: -0.01em; }
.nav-pop-card p { margin: 0; font-size: 0.78rem; line-height: 1.45; color: var(--text-mid); }

/* --- wariant listy linkow --- */
.nav-pop-list { display: flex; flex-direction: column; min-width: 190px; }
.nav-pop-list a {
  padding: 9px 12px;
  border-radius: 10px;
  font-size: 0.9rem;
  color: var(--text-mid);
  transition: color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
}
.nav-pop-list a:hover { color: var(--text-hi); background: rgba(150, 180, 255, 0.07); }

/* --- stopka panelu: drobne linki pod siatka --- */
.nav-pop-foot { display: flex; gap: 6px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-mid); }
.nav-pop-foot a {
  flex: 1;
  padding: 7px 10px;
  border-radius: 9px;
  font-size: 0.82rem;
  color: var(--text-mid);
  transition: color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
}
.nav-pop-foot a:hover { color: var(--text-hi); background: rgba(150, 180, 255, 0.07); }

/* Strzalka pod pozycja nawigacji, ktora ma menu. Obraca sie przy otwarciu. */
a[data-nav-menu]::after {
  content: '';
  display: inline-block;
  width: 6px; height: 6px;
  margin-left: 8px;
  border-right: 1.4px solid currentColor;
  border-bottom: 1.4px solid currentColor;
  transform: translateY(-2.5px) rotate(45deg);
  opacity: 0.45;
  transition: transform var(--dur-1) var(--ease-out), opacity var(--dur-1) var(--ease-out);
}
a[data-nav-menu][aria-expanded="true"]::after { transform: translateY(0.5px) rotate(-135deg); opacity: 0.85; }

@media (prefers-reduced-motion: reduce) {
  .nav-pop, .nav-pop-panel, a[data-nav-menu]::after { transition: none; }
}

#mobile-menu {
  overflow-y: auto;
  overscroll-behavior: contain;
  justify-content: center;
  justify-content: safe center;
}

/* ---------- Menu mobilne na niskim ekranie ---------------------------------
   Telefon w poziomie. Zmierzone przy 812x375: tresc menu ma 390px bez
   zalogowania i 456px po zalogowaniu (dochodzi link do koszyka), a ekran ma 375.
   Dol byl uciety, wiec przycisk logowania lezal pod krawedzia - menu dalo sie
   przewinac, ale pelnoekranowe menu, w ktorym trzeba scrollowac do "Zaloguj",
   jest zle zrobione.

   Zapytanie pyta o WYSOKOSC, bo telefon w poziomie ma 812px szerokosci i zadne
   zapytanie o szerokosc by go nie zlapalo. Prog 520px oddziela orientacje
   pozioma (320-430px wysokosci) od pionowej (568px i wiecej) z duzym zapasem,
   wiec chowanie paska adresu nie moze go przypadkiem przekroczyc. */

/* ---------- Menu konta WEWNATRZ menu mobilnego ------------------------------
   .user-menu jest rozwijakiem desktopowym: position:absolute, top:100%+10px,
   right:0 - i nie mialo zadnej reguly mobilnej. W menu pelnoekranowym przycisk
   konta stoi na dole wysrodkowanej listy, wiec popover otwieral sie pod nim, a
   pod nim jest juz tylko krawedz ekranu. Stad ucinanie i przewijanie.

   Pelnoekranowe menu nie potrzebuje plywajacej warstwy nad soba - ma caly ekran.
   Tutaj wiec pozycje konta ROZWIJAJA SIE W LISCIE, jak w aplikacji mobilnej:
   przycisk zostaje przelacznikiem (strzalka nadal sie obraca), a wiersze
   dokladaja sie do menu zamiast wisiec nad nim.

   display:none zamiast opacity, bo element statyczny zawsze zajmuje miejsce -
   samo wygaszenie zostawiloby dziure w liscie. Kosztem jest brak przejscia przy
   rozwijaniu, co na dotyku i tak jest niezauwazalne. */
#mobile-menu .user-menu-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
}
#mobile-menu .user-menu {
  position: static;
  display: none;
  /* 220px, nie 320. Zmierzone przy 412px szerokosci ekranu: pozycje menu maja
     93-133px, przycisk konta 89px, a panel mial 320px - dwa i pol raza wiecej
     niz najszersza pozycja i niemal cala szerokosc ekranu, podczas gdy reszta
     menu jest waska wysrodkowana kolumna. Stad wrazenie, ze panel nie nalezy do
     tego menu. 220px miesci "Subscription" z ikona i trzyma sie kolumny. */
  width: min(220px, 100%);
  min-width: 0;
  margin-top: 6px;
  opacity: 1;
  transform: none;
  pointer-events: auto;
  /* Cien unosil warstwe nad trescia - w liscie nie ma nad czym sie unosic. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
  transition: none;
}
#mobile-menu .user-menu-wrap.is-open .user-menu { display: flex; flex-direction: column; }
/* Wiersze konta sa podrzedne wobec pozycji nawigacji, wiec nie krzycza ich
   rozmiarem - inaczej "Wyloguj" wazylby tyle co "Aplikacja". */
#mobile-menu .user-menu a,
#mobile-menu .user-menu button { font-size: 0.98rem; }

/* Na niskim ekranie rozwiniete konto dokłada trzy wiersze do i tak ciasnej
   listy, wiec one tez musza zejsc. */

/* ---------- Menu mobilne skaluje sie z wysokoscia ekranu --------------------
   Najgorszy przypadek to zalogowany uzytkownik z ROZWINIETYM kontem: 5 pozycji
   nawigacji + koszyk + przycisk konta + naglowek konta + subskrypcja +
   wylogowanie. Zmierzone w pelnym rozmiarze: 632px. Miesci sie na 812px ekranu,
   ale na 568px (iPhone SE) brakowalo 96px i menu wymagalo przewijania.

   Zamiast dokladac kolejny prog, wartosci skaluja sie CIAGLE wzgledem wysokosci
   okna. Gorne granice clamp sa dokladnie tym, co bylo wczesniej, wiec na wysokim
   ekranie nic sie nie zmienia; ponizej menu schodzi plynnie zamiast przeskakiwac
   na progu. 2.46vh to 20px przy 812px wysokosci, czyli obecne 1.25rem - stad ta
   liczba, a nie okragla. */
#mobile-menu {
  gap: clamp(3px, 0.9vh, 8px);
  padding-block: clamp(14px, 3vh, 32px);
}
#mobile-menu a {
  padding: clamp(6px, 1.6vh, 13px) 26px;
  font-size: clamp(1rem, 2.46vh, 1.25rem);
}
#mobile-menu [data-discord-login] { margin-top: clamp(6px, 2vh, 18px); }
#mobile-menu .user-menu a,
#mobile-menu .user-menu button {
  font-size: clamp(0.88rem, 2vh, 0.98rem);
  padding-block: clamp(6px, 1.3vh, 10px);
  /* Wewnetrzny margines MUSI wrocic do 13px z reguly bazowej .user-menu a.
     "#mobile-menu a" wyzej ma specyficznosc identyfikatora (1,0,1) i bije klase
     (0,1,1), wiec narzucalo wierszom konta swoje 26px - po obu stronach, czyli
     52px z panelu szerokiego na 220px. Przy justify-content:space-between tekst
     i ikona ladowaly na tych wewnetrznych krawedziach, czyli oba wciagniete do
     srodka: ikona stala 33px od krawedzi panelu zamiast 13. */
  padding-inline: 13px;
}
#mobile-menu .user-menu-head { padding: clamp(6px, 1.3vh, 10px) 13px clamp(7px, 1.5vh, 12px); }

/* ---------- Telefon w poziomie: uklad poziomy ------------------------------
   MUSI zostac na koncu pliku. Zapytania medialne NIE podnosza specyficznosci,
   wiec blok clamp wyzej nadpisywal te wartosci mimo mniejszej szczegolowosci -
   decydowala kolejnosc. Pierwsza wersja tej reguly stala przed clampami i przez
   to nie robila nic.

   Tu menu przestaje byc kolumna. Ekran ma 812px szerokosci i 375px wysokosci -
   wysokosci brakuje, szerokosci jest w nadmiarze, wiec pozycje ida w RZAD i
   zawijaja sie. Szesc linkow nawigacji miesci sie w jednej linii (~520px z 732px
   dostepnych), zamiast zajmowac szesc pieter.

   Zmierzone: uklad kolumnowy dawal 430px tresci przy rozwinietym koncie, potem
   379px po sciscnieciu - wciaz ponad 375px ekranu. Sciskanie dalej zjadaloby
   czytelnosc dla 14px. Uklad poziomy schodzi do ~140px i problem znika z duzym
   zapasem, zamiast byc rozwiazany na styk.

   Konto dostaje flex-basis:100%, wiec lamie linie i rozwija sie pod paskiem
   linkow, a nie w jego srodku. */
@media (max-height: 520px) {
  #mobile-menu {
    flex-flow: row wrap;
    align-content: center;
    justify-content: center;
    gap: 6px 8px;
    padding: 12px 40px;
  }
  #mobile-menu a { flex: 0 0 auto; padding: 6px 16px; font-size: 0.95rem; }
  #mobile-menu .user-menu-wrap { flex: 0 0 100%; margin-top: 4px; }
  #mobile-menu [data-discord-login] { margin-top: 0; padding-block: 7px; font-size: 0.9rem; }
  #mobile-menu .user-menu { margin-top: 5px; width: min(240px, 100%); }
  #mobile-menu .user-menu-head { padding: 5px 12px 6px; margin-bottom: 2px; }
  #mobile-menu .user-menu a,
  #mobile-menu .user-menu button { font-size: 0.88rem; padding-block: 6px; }
  #mobile-menu-close { top: 8px; right: 8px; }
}

/* ---------- Pasek ogloszenia na waskim ekranie -----------------------------
   .announce jest kontenerem flex z zawijaniem, a jego trzy elementy - ikona,
   zdanie i link - sa OSOBNYMI jednostkami zawijania. Gdy zdanie nie miesci sie
   obok ikony, ikona zostaje sama w calej linii, zdanie zajmuje dwie, link
   trzecia: cztery linie i 103px wysokosci na ekranie 375px. Zmierzone na /pl/.

   Dla zdania z ikona w srodku i linkiem na koncu flex jest zlym narzedziem -
   to jest akapit, nie uklad. W bloku ikona i link sa zwyklymi elementami
   liniowymi, wiec plyna razem z tekstem i zadne z nich nie moze dostac wlasnej
   linii. Odstepy przez marginesy, bo gap w bloku nie dziala.

   Przy 901px i wyzej zostaje flex - tam wszystko miesci sie w jednej linii i
   wysrodkowanie flexem jest w porzadku. */
@media (max-width: 900px) {
  .announce {
    display: block;
    text-align: center;
    padding-inline: 14px;
    text-wrap: pretty;
  }
  .announce svg {
    display: inline;
    vertical-align: -2px;
    margin-right: 6px;
  }
  .announce a { margin-left: 6px; }
}

/* Najwezsze telefony: samo przejscie na blok nie wystarcza, gdy polskie zdanie
   jest o kilkanascie znakow dluzsze od angielskiego. Czcionka monospace przy
   0.06em odstepu miedzy znakami zjada duzo szerokosci - stad lekkie zejscie
   obu, zamiast skracania tekstu. */
@media (max-width: 380px) {
  .announce { font-size: 0.7rem; letter-spacing: 0.03em; padding-inline: 10px; }
}

/* Jedno zdanie pod cennikiem, na /utility/ i /services/.
   Celowo NIE mowi "cena zawiera VAT". Prospira nie ma zarejestrowanej
   dzialalnosci - build-legal.py zglasza SELLER_LEGAL jako nieustawione przy
   kazdym buildzie - wiec deklaracja podatkowa bylaby nieprawdziwa. To zdanie
   odpowiada na te sama obawe kupujacego ("ile naprawde zaplace"), mowiac
   wylacznie to, co jest pewne: kwota na ostatnim ekranie bedzie ta sama.
   Polskie tlumaczenie dopisuje BLIK-a, bo Stripe pokazuje go WYLACZNIE gdy
   pozycja jest juz w zlotowkach, czyli tylko w polskim checkoucie. */
.pay-note {
  margin: clamp(20px, 3vw, 28px) 0 0;
  text-align: center;
  font-size: 0.9rem;
  line-height: 1.55;
  color: var(--text-low);
}

/* Druga polowa etykiety przycisku logowania - "with Discord" / "przez Discord".

   Znika ponizej 1280 px i to jest wylacznie kwestia miejsca. Prog jest CELOWO
   wyzszy niz prog menu desktopowego (1120 px): gdyby oba byly rowne, tuz nad
   progiem menu wracalaby dluga etykieta i naglowek natychmiast siadalby na swoje
   minimum. Rozsuniete daja plynne przejscie - przy 1120 px marginesy wokol
   nawigacji maja po 61 px, przy 1280 po 115 px, a nad 1280, juz z dluga
   etykieta, po 68 px.

   Znaczenie nie ginie: logo Discorda zostaje na przycisku, a pelna nazwa siedzi
   w aria-label, wiec czytnik ekranu zawsze mowi calosc. */
@media (max-width: 1279px) {
  .nav-login-long { display: none; }
}
