/* ==========================================================================
   SAMYAR BEATZ — design system  (Phase 9 rebuild)

   The previous build was monochrome and correct but structurally dated:
   hard 90° corners everywhere, flat single-blur shadows, condensed all-caps
   display type, borders doing the work that elevation should do, and a
   footer that floated in mid-page on any short document. This is a rebuild,
   not a re-skin.

   FIVE RULES, and everything below follows from them:

   1. DEPTH COMES FROM LIGHT, NOT FROM LINES.
      Surfaces are separated by a ladder of near-identical greys plus a
      two-part shadow (tight contact + wide ambient). Hairline borders exist
      only where a real edge is being described — the top bevel of a glass
      panel, the rule under a table head. A box outlined on all four sides
      is the single most 2010 thing a UI can do.

   2. GLASS IS A MATERIAL, NOT A FILTER.
      backdrop-filter alone reads as "a div with blur on it". Real glass
      needs three layers: the blur, a translucent body so text stays
      readable over anything, and a 1px inner highlight along the TOP edge
      where light catches the bevel. That highlight is always white, in both
      themes — a light source does not invert because the page did.

   3. SOFT 3D IS A 6% GRADIENT, NOT A BEVEL.
      The One UI-style tiles (--tile) are a 145° gradient between two
      adjacent greys with an inset highlight above and an inset shadow
      below. Kept deliberately shallow; on a monochrome palette anything
      stronger stops looking like machined metal and starts looking like
      plastic from a 2008 web template.

   4. HIERARCHY COMES FROM TONE AND SIZE, NOT FROM SHOUTING.
      The off-white ladder (--text → --text-soft → --text-dim →
      --text-faint) carries emphasis. ALL-CAPS is reserved for genuinely
      label-like text: eyebrows, table headers, chip labels. Headlines are
      sentence case in a geometric grotesk with tight negative tracking.

   5. MOTION IS SHORT AND EASED-OUT.
      120–260ms on a decelerating curve. Anything an element does on hover
      must be reversible in the same beat, and nothing animates layout —
      transforms and opacity only, so hovering a 24-card grid never triggers
      a reflow.

   TWO MODES. Every token here is redefined per mode by the block
   ThemeSettings.theme_css() injects into <head>, keyed on html[data-theme].
   The values in :root below are the fallback if that block is ever missing,
   so they are the real dark palette rather than placeholders.
   ========================================================================== */

:root {
  /* -- Palette ------------------------------------------------------- */
  --bg: #09090b;
  --bg-elevated: #18181b;
  --bg-elevated-2: #27272a;
  --surface-1: #101013;
  --surface-2: #16161a;
  --surface-3: #1e1e22;
  --border: #1f1f23;
  --border-bright: #33333a;

  --text: #f4f4f5;
  --text-soft: #d4d4d8;
  --text-dim: #a1a1aa;
  --text-faint: #71717a;

  --accent: #e4e4e7;
  --accent-bright: #fafafa;
  --accent-ink: #09090b;
  --accent-wash: rgba(228, 228, 231, 0.10);

  /* -- Material ------------------------------------------------------ */
  --glass: rgba(24, 24, 27, 0.55);
  --glass-strong: rgba(24, 24, 27, 0.74);
  --glass-border: rgba(244, 244, 245, 0.08);
  --glass-hi: rgba(255, 255, 255, .10);
  --glass-blur: 22px;

  --tile: linear-gradient(145deg, #2a2a2f, #131316);
  --tile-edge: inset 0 1px 0 rgba(255,255,255,.09), inset 0 -1px 0 rgba(0,0,0,.35);

  --elev-1: 0 1px 2px rgba(0,0,0,.5), 0 2px 10px rgba(0,0,0,.35);
  --elev-2: 0 2px 6px rgba(0,0,0,.55), 0 14px 36px rgba(0,0,0,.45);
  --elev-3: 0 4px 12px rgba(0,0,0,.6), 0 28px 64px rgba(0,0,0,.55);
  --shadow-soft: var(--elev-1);

  /* -- Status. Not identity: deliberately desaturated so they read as
        state rather than as brand colour. Never the only cue — always
        alongside an icon and wording. ---------------------------------- */
  --success: #6b8f7b;
  --danger: #a86a6a;
  --warning: #9a8f6b;

  /* -- Type ---------------------------------------------------------- */
  --font-display: "Space Grotesk", "Inter", ui-sans-serif, -apple-system, "Segoe UI", sans-serif;
  --font-body: "Inter", ui-sans-serif, -apple-system, "Segoe UI", "Helvetica Neue", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;

  /* -- What monospace is FOR --------------------------------------------
     Two semantic aliases rather than reaching for --font-mono directly, so
     the decision lives here instead of being re-made at 31 call sites.

     The stylesheet used to set monospace on prices, category lines, licence
     notes, deal badges, table headers, every eyebrow and every spec label.
     Monospace reads as *terminal*, and at that density it was the main
     reason the storefront felt like a developer tool rather than a
     high-end shop — more than any individual word was.

     --font-numeric  a readout: a duration, a BPM, a file size, an index.
                     Fixed advance widths genuinely help here, because the
                     digits line up between rows and do not jitter as a
                     timer counts.
     --font-label    a small caps label. Body face, tracked out. Inter needs
                     a touch more weight than JetBrains Mono at 10px to hold
                     the same presence, which is why the label rules carry
                     500-600 rather than 400. */
  --font-numeric: var(--font-mono);
  --font-label: var(--font-body);

  /* -- Radius scale. Derived from one admin value; see
        ThemeSettings._radius(). -------------------------------------- */
  --r-xs: 6px;
  --r-sm: 10px;
  --r-md: 16px;
  --r-lg: 24px;
  --r-xl: 32px;
  --r-pill: 999px;
  --radius: 16px;

  /* -- Space. A 4px grid; the names are t-shirt sizes so a value can be
        re-tuned without renaming every use. --------------------------- */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px;
  --s-5: 24px; --s-6: 32px; --s-7: 48px; --s-8: 72px; --s-9: 104px;
  /* Legacy names from the previous build, still referenced by a few
     component blocks. Mapped rather than find-and-replaced so this file
     stays reviewable. */
  --space-1: var(--s-1); --space-2: var(--s-3); --space-3: var(--s-5);
  --space-4: var(--s-6); --space-5: var(--s-7); --space-6: var(--s-8);

  --header-height: 68px;
  --player-height: 84px;
  --wrap: 1240px;

  /* -- Vertical rhythm ----------------------------------------------------
     Two tokens rather than raw --s-* at every call site, because these are
     the values that decide whether the site reads as generous or cramped,
     and they need to be tunable in one place.

     --page-top is the air between the sticky header and the first thing on
     a page. It was a flat 48px, which put a 46px H1 within a third of its
     own height of the header — legible, but it read as tucked under the nav
     rather than as the start of a document.

     --section-gap is the distance between one homepage section and the
     next. Fixed 72px/48px meant the mobile rhythm was tighter exactly where
     sections need more separation, because there is no horizontal grouping
     left to do the work.

     Both are clamped rather than switched at a breakpoint: a 900px tablet
     should not get the phone's spacing just because it is under 1024. */
  --page-top: clamp(52px, 6vw, 88px);
  --section-gap: clamp(64px, 7vw, 96px);

  /* Minimum hit areas. WCAG 2.5.8 asks for 24px; every platform HIG asks
     for 44. These sit between the two for dense controls and at 44 for
     anything a thumb reaches for on a phone. */
  --tap-min: 40px;
  --tap-sm: 36px;

  /* -- Motion -------------------------------------------------------- */
  --ease-out: cubic-bezier(.22, 1, .36, 1);        /* decelerating */
  --ease-spring: cubic-bezier(.34, 1.56, .64, 1);  /* slight overshoot */
  --t-fast: 130ms;
  --t-base: 220ms;
  --theme-ease: var(--t-base) var(--ease-out);
}

/* ==========================================================================
   RESET & BASE
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  /* Anchors and skip-links must not land under the sticky header. */
  scroll-padding-top: calc(var(--header-height) + var(--s-4));
  -webkit-text-size-adjust: 100%;
}

/* STICKY FOOTER. The old build let <footer> sit wherever content ended, so
   any short page — About, a 404, an empty cart — showed the footer floating
   in the middle of the viewport with dead space beneath it. A flex column
   on <body> plus `flex: 1` on the main region pins it to the bottom without
   the absolute-positioning hacks that break as soon as content grows.
   100dvh, not 100vh: on mobile Safari the vh unit measures the viewport
   with the URL bar retracted, which leaves a gap on first paint. */
body {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.65;
  letter-spacing: -0.006em;
  margin: 0;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* The variable fonts carry real weights across the whole axis; without
     this, a browser that hasn't finished loading them synthesises a fake
     bold that reflows when the true face lands. */
  font-synthesis-weight: none;
  /* Contains the full-bleed hero's vw overhang. See .hero-video. */
  overflow-x: clip;
}
@supports not (min-height: 100dvh) { body { min-height: 100vh; } }

/* The growth region. `flex: 1 0 auto` rather than `flex: 1`: the shorthand
   sets flex-basis to 0%, which makes the main column ignore its own content
   height in some flex implementations and collapse behind the footer.

   The top padding is not decoration. Without it the first heading on every
   page started immediately below the sticky header with about 20px of air,
   so page titles read as tucked under the nav rather than as the start of
   a page. Full-bleed heroes cancel it (see .hero-video). */
#page {
  flex: 1 0 auto;
  padding-top: var(--page-top);
  padding-bottom: var(--section-gap);
}

/* Reserve room so the floating player never covers the last row. */
body.player-active { padding-bottom: var(--player-height); }

/* Colour-only transitions, scoped to the elements that actually change.
   A blanket `transition: all` would also animate layout and drag the whole
   page through a repaint on every hover. */
body, .site-header, .card, .panel, .surface, .beat-card, .testimonial,
.cta-block, .mini-player, .btn, input, select, textarea, .stat-card,
.chip, .tile, .filter-bar, .spec-item {
  transition: background-color var(--theme-ease),
              border-color var(--theme-ease),
              color var(--theme-ease);
}

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

/* A hairline scrollbar in the palette. The default light scrollbar on a
   near-black page is a bright stripe down the edge of every screen. */
* { scrollbar-width: thin; scrollbar-color: var(--border-bright) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--border-bright); border-radius: var(--r-pill);
  border: 3px solid transparent; background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover { background-clip: content-box; background-color: var(--text-faint); }

.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 var(--s-5); }
.mono { font-family: var(--font-numeric); font-feature-settings: "tnum" 1; }

/* ==========================================================================
   TYPOGRAPHY
   Sentence case, geometric display face, tight negative tracking. The old
   build set every heading in condensed all-caps, which is what made the
   whole site read as a 2015 mixtape cover.
   ========================================================================== */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.03em;
  margin: 0 0 var(--s-3);
  text-wrap: balance;          /* no orphaned last word in a headline */
}
h1 { font-size: clamp(30px, 4.4vw, 46px); letter-spacing: -0.035em; }
h2 { font-size: clamp(22px, 2.6vw, 30px); }
h3 { font-size: 17px; letter-spacing: -0.02em; }
h4 { font-size: 15px; letter-spacing: -0.015em; }

p { margin: 0 0 var(--s-3); }
p:last-child { margin-bottom: 0; }
.lead { font-size: 17px; line-height: 1.6; color: var(--text-soft); }

a { color: var(--text); text-decoration: none; transition: color var(--t-fast) var(--ease-out); }
a:hover { color: var(--accent-bright); }

/* Small caps label. The ONE place all-caps still belongs. */
.eyebrow, .label {
  font-family: var(--font-label);
  font-size: 10px; font-weight: 600;
  letter-spacing: .15em; text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 var(--s-2);
}

/* Section heading. The old version stretched a 1px rule across the page
   after every title — a divider per section is visual noise when the
   sections are already separated by 72px of space. */
.section-title {
  font-size: clamp(20px, 2.4vw, 26px);
  letter-spacing: -0.028em;
  margin: 0 0 var(--s-2);
}
.section-head { margin: var(--section-gap) 0 var(--s-5); }
/* #page already supplies the top gap, so the FIRST block on a page starts
   flush. Stacking both put ~72px above every page title.

   This used to be written as a bare `.section-head:first-child`, which was
   the bug behind "sections run into each other". Homepage sections come in
   two shapes: some emit a loose `.section-head` straight into .wrap, others
   wrap themselves in <section class="deals-block"> and put the head inside.
   For the second shape the head IS a first child, so the unscoped rule
   stripped the top margin off every wrapped section on the page — deals,
   testimonials, FAQ and About all butted directly against the grid above
   them with 0px between. The scoped selectors below say what was meant:
   only the first block of the page, and only the head that opens it. */
#page > .wrap > h1:first-child,
#page > .wrap > .section-head:first-child,
#page > .wrap > :first-child > .section-head:first-child { margin-top: 0; }
.section-head .section-title { margin: 0; }
/* A .section-title used on its own, with no .section-head around it. The
   gap normally comes from .section-head's margin, so a bare one landed
   flush against whatever it followed — on the kit page "Tags" and
   "Credits" sat directly on top of the paragraph above them. */
.section-title:not(.section-head *) { margin-top: var(--s-7); }
#page > .wrap > .section-title:first-child { margin-top: 0; }
.section-sub { color: var(--text-dim); margin: var(--s-2) 0 0; max-width: 62ch; }
.section-cta { margin-top: var(--s-5); }

/* Header row with an action pinned right. */
.head-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-4); flex-wrap: wrap; }

.meta {
  color: var(--text-dim); font-size: 12px;
  font-family: var(--font-label); letter-spacing: 0;
  font-feature-settings: "tnum" 1;
}
.meta .dot { margin: 0 6px; color: var(--text-faint); }

/* Card metadata — "Alt R&B · 92 BPM · G Major" — gets the micro-type
   treatment: smaller, uppercase, widely tracked. Deliberately scoped to
   cards. `.meta` also carries ordinary sentences elsewhere ("Your cart will
   still be here afterwards"), and uppercasing a sentence is shouting. */
.beat-card .meta {
  /* 10px/.08em matches .sale-flag rather than inventing a third micro-type
     size. It is also the size at which the longest real line in the
     catalogue — "MELODIC TRAP · 128 BPM · G MAJOR" — still fits: at 11/.1em
     it truncated to "G MA…", and the key is something buyers filter on, so
     dropping it to gain tracking is a bad trade. */
  font-size: 10px; font-weight: 550;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.beat-card .meta .dot { margin: 0 5px; }

/* Product cards reuse .beat-card, but their metadata is a descriptive
   phrase — "Project File (FLP / DAW session) · 48 files · FL Studio" — not
   three short tags. Uppercase micro-type suits "DRILL · 148 BPM · A MINOR";
   applied to a sentence it both shouts and overruns, and that line was
   truncating to "…48 files" and losing the DAW. So: sentence case, and
   allowed a second line rather than clipped. */
.beat-card.product-card .meta {
  font-size: 11px; letter-spacing: .01em; text-transform: none;
  white-space: normal;
}

/* The separator belongs to the chunk on its LEFT — see the comment in
   products/_card.html. `white-space: nowrap` keeps each attribute and its
   trailing middot together, so a wrap can only ever happen at the space
   after the dot. */
.meta-parts > span { white-space: nowrap; }
.meta-parts > span:not(:last-child)::after {
  content: "\00b7";
  margin: 0 2px 0 6px;
  color: var(--text-faint);
}
.credit-line { font-family: var(--font-label); font-weight: 550; font-size: 12px; color: var(--text-dim); margin: 0 0 var(--s-4); }

/* ==========================================================================
   ICONS
   ========================================================================== */
.icon {
  width: 20px; height: 20px;
  display: block; flex: none;
  fill: none;                   /* stroke symbols; filled ones set their own */
}
.icon-sm { width: 16px; height: 16px; }
.icon-lg { width: 24px; height: 24px; }

/* ==========================================================================
   ACCESSIBILITY
   ========================================================================== */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-xs);
}
/* Never remove the ring — only reshape it. */
.btn:focus-visible, .chip:focus-visible { outline-offset: 3px; }

.skip-link {
  position: fixed; left: var(--s-4); top: var(--s-4); z-index: 200;
  transform: translateY(-200%);
  background: var(--accent); color: var(--accent-ink);
  padding: 10px 18px; border-radius: var(--r-sm);
  font-weight: 600; font-size: 13px;
  box-shadow: var(--elev-2);
  transition: transform var(--t-base) var(--ease-out);
}
.skip-link:focus { transform: translateY(0); color: var(--accent-ink); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ==========================================================================
   MATERIALS — the three reusable surfaces everything else is built from
   ========================================================================== */

/* Flat panel: a plane above the page. No border on all four sides. */
.surface {
  background: var(--surface-2);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-1);
}

/* Glass: header, floating player, controls over the hero video. */
.glass {
  background: var(--glass);
  backdrop-filter: blur(var(--glass-blur)) saturate(160%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
  border: 1px solid var(--glass-border);
  box-shadow: inset 0 1px 0 var(--glass-hi), var(--elev-2);
}

/* Soft-3D tile: One UI-style. Used for icon buttons, spec cells, stat
   cards — anything that should read as a physical chip on the surface. */
.tile {
  background: var(--tile);
  border-radius: var(--r-md);
  box-shadow: var(--tile-edge), var(--elev-1);
}

/* ==========================================================================
   BUTTONS
   Glass by default. The primary is a solid slab of --accent with --accent-ink
   on it — in a monochrome palette the accent IS an off-white, so the label
   has to go dark or the button is white-on-white.
   ========================================================================== */
/* `.btn` alone, deliberately NOT `.btn, button.btn`. The compound selector
   was a specificity trap: at (0,1,1) it silently outranked every single
   class that tries to restyle a button elsewhere in this file, and the
   first casualty was `.nav-toggle { display: none }` — the hamburger stayed
   on screen at every width. A lone class already beats the UA stylesheet,
   which is all the extra weight was ever buying. */
.btn {
  --btn-py: 11px;
  --btn-px: 20px;
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  padding: var(--btn-py) var(--btn-px);
  /* A floor, not a height: the padding still drives the box for anything
     taller. Without it a 12px label at 7px padding produced a 30px control
     on every card — under the 44px every platform HIG asks for, and it is
     the button people tap on a phone while walking. */
  min-height: var(--tap-min);
  font-family: var(--font-body);
  font-size: 13px; font-weight: 550; letter-spacing: -0.005em;
  line-height: 1.2;
  border-radius: var(--r-pill);
  border: 1px solid var(--glass-border);
  background: var(--glass-strong);
  backdrop-filter: blur(var(--glass-blur)) saturate(160%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
  box-shadow: inset 0 1px 0 var(--glass-hi), var(--elev-1);
  color: var(--text);
  cursor: pointer;
  white-space: nowrap;
  transition: transform var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out),
              background-color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out);
}
/* translateY on hover, scale on press: both are compositor-only, so a grid
   of 24 buttons costs nothing to hover across. */
.btn:hover { transform: translateY(-1px); box-shadow: inset 0 1px 0 var(--glass-hi), var(--elev-2); color: var(--text); border-color: var(--border-bright); }
.btn:active { transform: translateY(0) scale(.985); box-shadow: inset 0 1px 0 var(--glass-hi), var(--elev-1); }

.btn-primary {
  background: var(--accent);
  border-color: transparent;
  color: var(--accent-ink);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.28), var(--elev-1);
  font-weight: 600;
}
.btn-primary:hover { background: var(--accent-bright); color: var(--accent-ink); border-color: transparent; }

.btn-ghost {
  background: transparent; border-color: var(--border-bright);
  backdrop-filter: none; -webkit-backdrop-filter: none;
  box-shadow: none; color: var(--text-dim);
}
.btn-ghost:hover { background: var(--accent-wash); color: var(--text); box-shadow: none; }

.btn-sm { --btn-py: 8px; --btn-px: 16px; font-size: 12px; min-height: var(--tap-sm); }
.btn-lg { --btn-py: 15px; --btn-px: 30px; font-size: 15px; min-height: 52px; }
.btn-block { width: 100%; }

.btn[disabled], .btn:disabled {
  opacity: .4; cursor: not-allowed; transform: none !important; box-shadow: none;
}

/* Icon-only round button — the shape used by the theme toggle, the cart,
   and every social link. */
.btn-icon {
  --btn-py: 0; --btn-px: 0;
  width: 40px; height: 40px; padding: 0;
  border-radius: var(--r-pill);
  color: var(--text-dim);
}
.btn-icon:hover { color: var(--text); }
.btn-icon .icon { width: 18px; height: 18px; }

.btn-link {
  background: none; border: 0; padding: 0; box-shadow: none; backdrop-filter: none;
  color: var(--text-dim); font-family: var(--font-body); font-size: 13px;
  text-decoration: underline; text-underline-offset: 3px; cursor: pointer;
}
.btn-link:hover { color: var(--text); transform: none; box-shadow: none; }
.inline-form { display: inline; }

/* ==========================================================================
   CHIPS & BADGES
   ========================================================================== */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px;
  border-radius: var(--r-pill);
  background: var(--surface-3);
  border: 1px solid transparent;
  font-family: var(--font-label); font-weight: 550; font-size: 11px; letter-spacing: .04em;
  color: var(--text-dim);
  text-transform: uppercase;
}
a.chip:hover, .chip-active {
  background: var(--accent); color: var(--accent-ink); border-color: transparent;
}

.badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 9px; border-radius: var(--r-pill);
  background: var(--surface-3); color: var(--text-dim);
  font-family: var(--font-label); font-weight: 550; font-size: 10px;
  letter-spacing: .12em; text-transform: uppercase;
}
.badge-sold { background: color-mix(in srgb, var(--danger) 22%, transparent); color: var(--danger); }
.badge-free { background: color-mix(in srgb, var(--success) 22%, transparent); color: var(--success); }
.badge-popular { background: var(--accent); color: var(--accent-ink); }

/* ==========================================================================
   NOTICES & MESSAGES
   ========================================================================== */
.notice, .message {
  display: flex; align-items: flex-start; gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-radius: var(--r-md);
  background: var(--surface-2);
  box-shadow: var(--elev-1);
  font-size: 14px;
  /* The status cue is a left rail, not a full outline — the shape reads as
     "annotation on this block" rather than "boxed warning". */
  border-left: 3px solid var(--border-bright);
}
.notice { margin: 0 0 var(--s-5); }
.notice-warn { border-left-color: var(--warning); }

/* The flex row above assumes [cue] + [text]. When a notice holds two block
   paragraphs instead, they become two columns: the cart's "Log in to check
   out" panel put its hint alongside the CTA, squeezing the CTA to 174px so
   "or create an account." wrapped underneath the button. This modifier says
   "these are stacked blocks, not a row". */
.notice-stack { flex-direction: column; gap: var(--s-2); }

.messages { list-style: none; margin: var(--s-5) 0 0; padding: 0; display: grid; gap: var(--s-2); }
.message-success { border-left-color: var(--success); }
.message-error { border-left-color: var(--danger); }
.message-warning { border-left-color: var(--warning); }

/* ==========================================================================
   HEADER
   ========================================================================== */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: var(--glass);
  backdrop-filter: blur(var(--glass-blur)) saturate(180%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(180%);
  /* A bottom hairline only, plus the top-edge highlight. A full border
     would draw a box around the whole header. */
  border-bottom: 1px solid var(--glass-border);
}
.header-inner {
  display: flex; align-items: center; gap: var(--s-5);
  height: var(--header-height);
}

/* Logo. `height` is set on the image and width left auto so any logo
   aspect ratio works; the reserved box prevents the nav jumping when the
   image decodes. */
.wordmark {
  display: flex; align-items: center; gap: 10px; flex: none;
  font-family: var(--font-display); font-weight: 600;
  font-size: 17px; letter-spacing: -0.02em;
}
.wordmark:hover { color: var(--text); opacity: .82; }

/* Theme-aware wordmark. See templates/_logo.html for the three cases.
   `height` is fixed and `width: auto` so any logo aspect ratio works. */
.brand-logo { display: block; width: auto; }
.wordmark .brand-logo { height: 26px; }
.footer-logo .brand-logo { height: 30px; margin-bottom: var(--s-4); }
/* Case 2: monochrome mark, no light variant uploaded. Without this the
   shipped white-on-transparent wordmark is invisible on the light theme's
   near-white header — the logo was literally not there after one click of
   the toggle. */
[data-theme="light"] .brand-logo-auto { filter: invert(1); }
/* Case 1: a light variant WAS uploaded — swap rather than invert. */
.brand-logo-light { display: none; }
[data-theme="light"] .brand-logo-dark { display: none; }
[data-theme="light"] .brand-logo-light { display: block; }

.main-nav { display: flex; align-items: center; gap: var(--s-1); margin-left: auto; }
.main-nav a {
  position: relative;
  padding: 8px 14px; border-radius: var(--r-pill);
  font-size: 13px; font-weight: 500; letter-spacing: -0.005em;
  color: var(--text-dim);
  transition: color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out);
}
.main-nav a:hover { color: var(--text); background: var(--accent-wash); }
/* The current page is marked by a dot under the label, not by a border on
   the link box — it survives the pill shape and reads at a glance. */
.main-nav a[aria-current="page"] { color: var(--text); }
.main-nav a[aria-current="page"]::after {
  content: ""; position: absolute; left: 50%; bottom: 1px;
  width: 4px; height: 4px; margin-left: -2px;
  border-radius: 50%; background: var(--accent);
}
.main-nav .nav-cta {
  background: var(--accent); color: var(--accent-ink); font-weight: 600;
  padding: 8px 16px;
}
.main-nav .nav-cta:hover { background: var(--accent-bright); color: var(--accent-ink); }

/* The CTA keeps its own ink when it IS the current page.
   `.main-nav a[aria-current="page"]` above is (0,2,1) and beat the CTA's own
   (0,2,0) `color`, while leaving its `background` alone — so the pill was
   painted --accent and lettered --text. Since --accent is the inverted text
   colour, that is the same colour twice: "Sign Up" rendered as an empty
   black pill in light mode and an empty white one in dark, on the one page
   a visitor arriving from a shared registration link always lands on.
   This selector is (0,3,0), so it wins in both themes. */
.main-nav .nav-cta[aria-current="page"] { color: var(--accent-ink); }
/* Same reason for the current-page dot: --accent on an --accent pill is
   invisible. On the CTA it has to be drawn in the ink colour instead. */
.main-nav .nav-cta[aria-current="page"]::after { background: var(--accent-ink); }

.header-actions { display: flex; align-items: center; gap: var(--s-2); flex: none; }

.nav-cart { position: relative; }
.cart-count {
  position: absolute; top: -1px; right: -1px;
  min-width: 18px; height: 18px; padding: 0 5px;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--accent); color: var(--accent-ink);
  font-family: var(--font-label); font-size: 10px; font-weight: 500; line-height: 1;
  box-shadow: 0 0 0 2px var(--bg);
}

/* Theme toggle. Both icons are in the DOM and CSS picks; swapping them in
   JS would leave the button empty until scripts run and would need re-running
   after every hx-boost swap. The icon shown is the mode you would switch TO,
   not the one you are in — a control should advertise its outcome. */
.theme-toggle .icon-sun { display: none; }
.theme-toggle .icon-moon { display: block; }
[data-theme="light"] .theme-toggle .icon-sun { display: block; }
[data-theme="light"] .theme-toggle .icon-moon { display: none; }

/* Hamburger. Three bars that morph into an X — transform and opacity only,
   so the animation never touches layout. */
.nav-toggle { display: none; }
.nav-toggle-bars { display: block; }
.nav-toggle-bar {
  display: block; width: 17px; height: 1.5px; border-radius: 2px;
  background: currentColor;
  transition: transform var(--t-base) var(--ease-out), opacity var(--t-fast) linear;
}
.nav-toggle-bar + .nav-toggle-bar { margin-top: 4px; }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }

/* Logout is a POST form (Django 5 removed GET logout) but has to sit and
   look exactly like the links beside it. */
.nav-logout { display: inline; margin: 0; }
.nav-logout button {
  background: none; border: 0; padding: 8px 14px; border-radius: var(--r-pill);
  cursor: pointer; font: inherit; font-size: 13px; font-weight: 500;
  color: var(--text-dim);
  transition: color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out);
}
.nav-logout button:hover { color: var(--text); background: var(--accent-wash); }

/* ==========================================================================
   HERO
   ========================================================================== */
.hero {
  position: relative;
  padding: var(--s-9) 0 var(--s-8);
}
.hero h1 { margin-bottom: var(--s-4); }
.hero .tagline { color: var(--text-dim); font-size: 17px; max-width: 54ch; margin-bottom: var(--s-5); }
.hero-actions { display: flex; gap: var(--s-3); flex-wrap: wrap; }

/* Full-bleed video hero. `isolation: isolate` creates a stacking context so
   the negative z-index layers below sit behind the copy but never behind
   the page background. */
.hero-video {
  position: relative; isolation: isolate;
  min-height: min(82vh, 760px);
  display: grid; place-items: center; text-align: center;
  overflow: hidden;
  /* Full-bleed from inside .wrap. The negative margin is expressed in vw,
     which counts the scrollbar gutter, so on a scrolling page it overhangs
     by half a scrollbar on each side — hence `overflow-x: clip` on body.
     `clip` rather than `hidden` deliberately: `hidden` makes body a scroll
     container, which silently kills `position: sticky` on the header. */
  margin-inline: calc(50% - 50vw);
  padding-inline: calc(50vw - 50%);
  /* Cancels #page's top padding: a full-bleed hero has to start flush
     against the header, not below it with a band of page colour above the
     video.

     It must reference the SAME token #page uses. It was hardcoded to
     `--s-7` while #page moved to the clamped `--page-top`, so the two
     drifted apart and left 38px of page background above the hero. That
     was invisible in the dark theme — near-black on near-black — and
     showed as a grey stripe under the header in the light one. Any fixed
     value here is the same bug waiting to come back. */
  margin-top: calc(var(--page-top) * -1);
  /* NO negative bottom margin. Pulling the next section up under the
     hero's fading edge looked right on a tall desktop viewport and was
     broken everywhere else: .hero-video is `position: relative`, so it
     paints ABOVE the in-flow content that follows it, and on mobile the
     overlap swallowed the first section heading entirely. The gradient
     already resolves to --bg at 96%, so the seam closes without it. */
}
.hero-video > .hero-video-el,
.hero-video > .hero-video-poster {
  position: absolute; inset: 0; z-index: -2;
  width: 100%; height: 100%; object-fit: cover;
}
/* Two stacked overlays. The linear pass darkens for legibility and
   dissolves the bottom edge into --bg so the section ends without a seam;
   the radial pass is a vignette that pulls the eye to the centre. */
.hero-video::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background:
    radial-gradient(120% 80% at 50% 35%, transparent 0%, rgba(9,9,11,.55) 100%),
    linear-gradient(to bottom, rgba(9,9,11,.72) 0%, rgba(9,9,11,.52) 40%, var(--bg) 96%);
}
/* Light mode.
   The video footage is dark, so the copy on it must stay light in BOTH
   themes — there is no version of this where graphite text on a night-time
   photograph is readable. What changes is how the panel ENDS: on a light
   page a dark band running edge to edge and stopping dead reads as an
   unfinished image. The gradient resolves into --bg earlier and the whole
   section gets a rounded bottom, so it reads as a deliberate media panel
   sitting on the page rather than a slab bleeding off it. */
[data-theme="light"] .hero-video {
  min-height: min(70vh, 640px);
  border-bottom-left-radius: var(--r-xl);
  border-bottom-right-radius: var(--r-xl);
}
[data-theme="light"] .hero-video::after {
  background:
    radial-gradient(120% 80% at 50% 35%, transparent 0%, rgba(9,9,11,.40) 100%),
    linear-gradient(to bottom, rgba(9,9,11,.55) 0%, rgba(9,9,11,.38) 34%, var(--bg) 100%);
}
/* The rounded corners need something behind them, or the video's own square
   edge shows through at the clip. */
[data-theme="light"] .hero-video > .hero-video-el,
[data-theme="light"] .hero-video > .hero-video-poster {
  border-bottom-left-radius: var(--r-xl);
  border-bottom-right-radius: var(--r-xl);
}

/* Hero copy always sits on the darkened video in BOTH themes, so it is
   pinned to the light end of the ladder rather than following --text —
   which would turn graphite-on-video in light mode. */
.hero-copy { position: relative; z-index: 1; padding: var(--s-9) var(--s-5); width: 100%; }
.hero-video h1 {
  color: #fafafa;
  font-size: clamp(40px, 7.5vw, 84px);
  letter-spacing: -0.045em;
  font-weight: 600;
  margin-bottom: var(--s-4);
}
.hero-video .tagline {
  color: rgba(250,250,250,.72); margin-inline: auto; font-size: clamp(15px, 1.6vw, 18px);
}
/* The brand line above the name. Fixed white-alpha rather than a palette
   token because it sits on the video, which is dark in both themes. */
.hero-eyebrow {
  color: rgba(250,250,250,.62);
  font-size: 11px; letter-spacing: .2em;
  margin-bottom: var(--s-4);
}
.hero-video .hero-actions { justify-content: center; }
.hero-video .btn-ghost { color: rgba(250,250,250,.86); border-color: rgba(250,250,250,.28); }
.hero-video .btn-ghost:hover { background: rgba(250,250,250,.12); color: #fafafa; }

@media (prefers-reduced-motion: reduce) {
  /* A looping background video is exactly the decorative motion this query
     exists to suppress. The poster still carries the image. */
  .hero-video > .hero-video-el { display: none; }
}

/* Hero search — one glass pill. */
.hero-search {
  display: flex; align-items: center; gap: var(--s-2);
  max-width: 640px; margin: 0 auto var(--s-5);
  padding: 6px 6px 6px var(--s-5);
  border-radius: var(--r-pill);
  background: rgba(255,255,255,.07);
  backdrop-filter: blur(var(--glass-blur)) saturate(160%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
  border: 1px solid rgba(255,255,255,.14);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.16), var(--elev-2);
}
.hero-search > .icon { color: rgba(250,250,250,.5); }
/* Typed as [type="search"] to match the specificity of the global input
   rule in the FORMS block below. At the plainer `.hero-search input` this
   lost on source order and the field rendered as a bordered dark box
   floating inside the glass pill. */
.hero-search input[type="search"] {
  flex: 1; min-width: 0; width: auto;
  background: none; border: 0; box-shadow: none;
  color: #fafafa; padding: 12px 0;
  font-family: var(--font-body); font-size: 15px;
}
.hero-search input[type="search"]::placeholder { color: rgba(250,250,250,.5); }
.hero-search input[type="search"]:hover,
.hero-search input[type="search"]:focus { outline: none; box-shadow: none; border: 0; }
.hero-search .btn { flex: none; }
/* The submit carries both a word and a glyph; only one shows at a time.
   Desktop gets the word, narrow screens get the glyph (see the responsive
   block) — rendering both would be a button that says "Search 🔍". */
.hero-search .btn .icon { display: none; }

/* ==========================================================================
   GRID & CARDS
   ========================================================================== */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
  gap: var(--s-5);
}

.beat-card {
  position: relative;
  display: flex; flex-direction: column;
  background: var(--surface-2);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-1);
  overflow: hidden;
  transition: transform var(--t-base) var(--ease-out),
              box-shadow var(--t-base) var(--ease-out),
              background-color var(--theme-ease);
}
.beat-card:hover { transform: translateY(-4px); box-shadow: var(--elev-3); background: var(--surface-3); }
.beat-card:focus-within { box-shadow: var(--elev-2), 0 0 0 2px var(--accent); }

.beat-card .cover {
  position: relative; aspect-ratio: 1/1; overflow: hidden;
  background: var(--surface-3);
  display: grid; place-items: center;
  color: var(--text-faint); font-family: var(--font-label); font-weight: 550; font-size: 11px;
}
/* Absolute so the FAB and the flags can stack over it. It must repeat the
   centring, not just `display: block` — the "No cover" fallback text is a
   child of the LINK, not of .cover, so it was pinning itself to the
   top-left corner and clipping. */
.beat-card .cover-link { position: absolute; inset: 0; display: grid; place-items: center; }
.beat-card .cover img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform 480ms var(--ease-out);
}
.beat-card:hover .cover img { transform: scale(1.045); }

/* Play overlay. Hidden until hover on pointer devices; ALWAYS visible on
   touch, where there is no hover to reveal it — an invisible play button on
   a phone is just a missing play button. */
.cover-play {
  position: absolute; inset: 0; z-index: 2;
  display: grid; place-items: center;
  background: linear-gradient(to top, rgba(9,9,11,.55), transparent 62%);
  opacity: 0;
  transition: opacity var(--t-base) var(--ease-out);
  /* The overlay spans the whole cover, which sits on top of the link to the
     beat page. Without this it would swallow every click on the artwork and
     the card would stop being navigable. Only the button opts back in. */
  pointer-events: none;
}
.cover-play .play-fab { pointer-events: auto; }
.beat-card:hover .cover-play, .beat-card:focus-within .cover-play { opacity: 1; }
@media (hover: none) { .cover-play { opacity: 1; background: none; } }

.cover-play .play-fab {
  width: 52px; height: 52px; border-radius: 50%; padding: 0;
  display: grid; place-items: center; cursor: pointer;
  background: var(--glass-strong);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  border: 1px solid rgba(255,255,255,.20);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.25), var(--elev-2);
  color: var(--text);
  transform: scale(.88);
  transition: transform var(--t-base) var(--ease-spring), background-color var(--t-fast) var(--ease-out);
}
.beat-card:hover .play-fab { transform: scale(1); }
.play-fab:hover { background: var(--accent); color: var(--accent-ink); }
.play-fab .icon { width: 18px; height: 18px; margin-left: 2px; }   /* optical centring of the triangle */
.play-fab.is-playing .icon { margin-left: 0; }

/* Card interior. 24px of padding and a 12px stack gap: at 16/8 the title,
   the metadata line and the waveform were three rows of text touching each
   other, which read as one dense block rather than as three things. */
.card-body { padding: var(--s-5); display: flex; flex-direction: column; gap: var(--s-3); flex: 1; }
.card-title {
  font-size: 17px; font-weight: 600; letter-spacing: -0.022em;
  margin: 0; line-height: 1.25;
}
.card-title a { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* "from $29.99". The markup wraps the figure in <strong>, so the qualifier
   can stay quiet while the number carries the weight — otherwise "from"
   shouts as loudly as the price it qualifies. */
.card-price {
  font-size: 12px; font-weight: 450; color: var(--text-dim);
  display: inline-flex; align-items: baseline; gap: 5px;
}
.card-price strong {
  font-family: var(--font-display); font-size: 16px; font-weight: 600;
  letter-spacing: -0.025em; color: var(--text);
  font-feature-settings: "tnum" 1;
}
.card-price strong { color: var(--text); font-weight: 500; }
.card-price s { color: var(--text-faint); margin-right: 5px; }

/* The CTA row. `margin-top: auto` pins it to the bottom so cards of
   different heights line their buttons up; the padding-top is what stops the
   price and the button pressing against the waveform above them. */
.card-actions { display: flex; align-items: center; gap: var(--s-3); margin-top: auto; padding-top: var(--s-4); }
.card-actions form { margin: 0; }
.card-actions .btn { flex: none; }
.card-actions .card-price { margin-right: auto; }

.sale-flag {
  position: absolute; top: var(--s-3); left: var(--s-3); z-index: 3;
  padding: 4px 10px; border-radius: var(--r-pill);
  background: var(--accent); color: var(--accent-ink);
  font-family: var(--font-label); font-weight: 550; font-size: 10px; letter-spacing: .08em;
  box-shadow: var(--elev-1);
}
/* The same badge for use INSIDE a line of text — the licence table's price
   cell. `.sale-flag` is absolutely positioned to sit on a cover, so reusing
   it here would park it in the corner of whichever ancestor happened to be
   positioned. Same look, no positioning. */
.sale-tag {
  display: inline-block; vertical-align: middle; margin-left: var(--s-2);
  padding: 3px 8px; border-radius: var(--r-pill);
  background: var(--accent); color: var(--accent-ink);
  font-family: var(--font-label); font-weight: 550; font-size: 10px; letter-spacing: .08em;
}

.card-flags { position: absolute; top: var(--s-3); right: var(--s-3); z-index: 3; display: flex; gap: 5px; }

/* --- Sale strip -----------------------------------------------------------
   Full-bleed under the header on every page. Accent-on-ink rather than a
   tinted panel: it has to read as an announcement at a glance from the
   corner of the eye, and a quiet strip is one nobody sees. */
.sale-strip {
  background: var(--accent); color: var(--accent-ink);
  font-family: var(--font-label); font-size: 12px; letter-spacing: .02em;
}
.sale-strip-inner {
  display: flex; align-items: center; justify-content: center; gap: var(--s-3);
  padding: var(--s-2) var(--s-4); text-align: center;
}
.sale-strip-name { font-weight: 650; letter-spacing: .06em; text-transform: uppercase; }
.sale-strip-offer { opacity: .92; }
.sale-strip a {
  color: inherit; text-decoration: underline; text-underline-offset: 2px;
}
@media (max-width: 600px) {
  /* The link is the first thing to go — the strip's job is to say a sale is
     on, and every price on the page below is already a way in. */
  .sale-strip a { display: none; }
  .sale-strip-inner { gap: var(--s-2); }
}

/* ==========================================================================
   WAVEFORM & PLAYER
   Bars are centre-mirrored (flex align-items:center) with rounded caps —
   the previous build's 1px square bottom-aligned bars read as a bar chart,
   not as audio.
   ========================================================================== */
.wf-row { display: flex; align-items: center; gap: var(--s-3); }

.wf-playbtn {
  flex: none; width: var(--tap-sm); height: var(--tap-sm); border-radius: 50%;
  display: inline-grid; place-items: center;
  background: var(--tile);
  border: 0;
  box-shadow: var(--tile-edge), var(--elev-1);
  color: var(--text); cursor: pointer;
  transition: transform var(--t-fast) var(--ease-out), background var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.wf-playbtn:hover { transform: scale(1.06); }
.wf-playbtn:active { transform: scale(.95); }
.wf-playbtn.is-playing { background: var(--accent); color: var(--accent-ink); box-shadow: var(--elev-1); }
.wf-playbtn .icon { width: 13px; height: 13px; margin-left: 1px; }
.wf-playbtn.is-playing .icon { margin-left: 0; }

.wf-bars {
  position: relative;
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: 3px;
  /* The scrub target, so it gets the full tap minimum rather than matching
     the button beside it — a 34px-tall strip is a hard thing to hit
     accurately on a phone, and hitting it inaccurately means seeking to the
     wrong place. The bars are drawn centred, so the extra height is margin
     around the waveform, not taller bars. */
  height: var(--tap-min); cursor: pointer;
  /* player.js downsamples peaks to the available width, but overflow is the
     backstop — a bar must never spill into the next card. */
  overflow: hidden;
  touch-action: pan-y;   /* vertical page scrolling still works over a waveform */
}
.wf-bar {
  flex: 1; min-width: 3px;
  border-radius: var(--r-pill);
  background: var(--wf-idle, rgba(255,255,255,.16));
  transition: background-color 90ms linear;
}
[data-theme="light"] .wf-bar { background: rgba(9,9,11,.16); }
.wf-bar.is-played { background: var(--accent); }

/* No peaks extracted yet — visibly inert, never a fake waveform (§12). */
.wf-placeholder { cursor: default; opacity: .45; }
.wf-placeholder .wf-bar { background: var(--border-bright); }

.wf-time {
  font-family: var(--font-numeric); font-size: 11px; color: var(--text-dim);
  min-width: 36px; text-align: right; flex: none;
  font-feature-settings: "tnum" 1;   /* the readout must not jitter as digits change */
}

/* Scrub cursor + timestamp badge. Positioned from JS via a custom property
   rather than by writing `left` on two elements: one style write per frame,
   and the units stay in the stylesheet. */
.wf-cursor {
  position: absolute; top: 0; bottom: 0; width: 1.5px;
  left: var(--wf-cursor-x, 0);
  border-radius: 2px; background: var(--text);
  opacity: 0; pointer-events: none;
  transition: opacity var(--t-fast) linear;
}
.wf-bars:hover .wf-cursor, .wf-bars.is-scrubbing .wf-cursor { opacity: .9; }

.wf-badge {
  position: absolute; bottom: calc(100% - 2px);
  left: var(--wf-cursor-x, 0); transform: translateX(-50%);
  padding: 3px 7px; border-radius: var(--r-xs);
  background: var(--accent); color: var(--accent-ink);
  font-family: var(--font-label); font-weight: 550; font-size: 10px; line-height: 1.3;
  opacity: 0; pointer-events: none; white-space: nowrap;
  box-shadow: var(--elev-1);
  transition: opacity var(--t-fast) linear;
}
.wf-bars:hover .wf-badge, .wf-bars.is-scrubbing .wf-badge { opacity: 1; }
/* No hover state on touch — it would stick after a tap and never clear. */
@media (hover: none) {
  .wf-bars:hover .wf-cursor, .wf-bars:hover .wf-badge { opacity: 0; }
}
.wf-bars.is-scrubbing { cursor: grabbing; }

/* --- The persistent bar --------------------------------------------------
   Floating and inset from the window edges rather than a full-width slab
   welded to the bottom. It reads as an object above the page, which is what
   it is: it outlives every navigation. */
.mini-player {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  padding: 0 var(--s-4) var(--s-4);
  transform: translateY(140%);
  transition: transform 320ms var(--ease-out);
  pointer-events: none;                 /* the padded gutter must not eat clicks */
}
.mini-player.is-visible { transform: translateY(0); }
.mini-player-inner {
  pointer-events: auto;
  max-width: var(--wrap); margin: 0 auto;
  display: flex; align-items: center; gap: var(--s-4);
  height: 64px; padding: 0 var(--s-4);
  border-radius: var(--r-lg);
  background: var(--glass-strong);
  backdrop-filter: blur(28px) saturate(180%);
  -webkit-backdrop-filter: blur(28px) saturate(180%);
  border: 1px solid var(--glass-border);
  box-shadow: inset 0 1px 0 var(--glass-hi), var(--elev-3);
}

/* Three-zone layout: identity | transport | scrub | actions. The transport
   is centred in the bar rather than tucked at one end, which is what makes
   it read as a player instead of a notification strip. */
.mini-player-inner { display: grid; grid-template-columns: minmax(0,1fr) auto minmax(0,1.6fr) auto; }

.mp-identity { display: flex; align-items: center; gap: var(--s-3); min-width: 0; }
.mp-art {
  flex: none; width: 44px; height: 44px; border-radius: var(--r-sm);
  overflow: hidden; background: var(--surface-3);
  display: grid; place-items: center;
  box-shadow: var(--tile-edge);
}
.mp-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.mp-art-fallback {
  font-family: var(--font-display); font-size: 17px; font-weight: 600;
  color: var(--text-faint);
}
.mp-info { min-width: 0; display: flex; flex-direction: column; line-height: 1.35; }
.mp-title { font-size: 13px; font-weight: 600; letter-spacing: -0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mp-meta { font-family: var(--font-label); font-weight: 550; font-size: 10px; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The subtitle doubles as the player's error line — see failed() in
   player.js. It is the only always-on-screen surface the player owns, and a
   silent failure here is what "the play button does nothing" actually was. */
.mp-meta.is-error { color: var(--danger); }

.mp-transport { display: flex; align-items: center; gap: var(--s-2); }
.mp-scrub { display: flex; align-items: center; gap: var(--s-3); min-width: 0; }
.mp-actions { display: flex; align-items: center; gap: 2px; }

/* One button shape for every secondary control. */
.mp-btn {
  flex: none; width: 36px; height: 36px; border-radius: 50%; border: 0; padding: 0;
  display: grid; place-items: center;
  background: transparent; color: var(--text-dim); cursor: pointer;
  transition: color var(--t-fast) var(--ease-out),
              background-color var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out);
}
.mp-btn:hover { color: var(--text); background: var(--accent-wash); }
.mp-btn:active { transform: scale(.92); }
.mp-btn .icon { width: 19px; height: 19px; }

/* `display: grid` above would otherwise beat the hidden attribute, and the
   queue controls are hidden until there IS a queue. */
.mp-btn[hidden] { display: none; }

/* End of the queue: still there, visibly not available. Removing them at
   the last track would shift the transport under the listener's cursor. */
.mp-btn:disabled { opacity: .32; cursor: default; }
.mp-btn:disabled:hover { color: var(--text-dim); background: transparent; }
.mp-btn:disabled:active { transform: none; }

/* Shuffle is a mode, not an action — it has to read as ON at a glance,
   which a hover-coloured icon does not. */
.mp-shuffle.is-on { color: var(--text); background: var(--accent-wash); }

/* The play button is deliberately bigger than its neighbours — the primary
   action in a cluster has to be identifiable without reading the icons. */
.mp-play { width: 44px; height: 44px; }
.mp-play .icon { width: 16px; height: 16px; }

.mp-elapsed, .mp-time {
  font-family: var(--font-numeric); font-size: 11px; color: var(--text-dim);
  min-width: 40px; flex: none; font-feature-settings: "tnum" 1;
}
.mp-elapsed { text-align: left; }
.mp-time { text-align: right; }

/* --- Volume ---------------------------------------------------------------
   The slider is collapsed until the group is hovered OR focused. Keeping it
   permanently open would crowd the transport, and hiding it behind hover
   alone would put it out of reach of a keyboard. */
.mp-volume { display: flex; align-items: center; }
.mp-volume-slider {
  width: 0; opacity: 0; padding: 0; margin: 0;
  transition: width var(--t-base) var(--ease-out), opacity var(--t-fast) linear,
              margin var(--t-base) var(--ease-out);
}
.mp-volume:hover .mp-volume-slider,
.mp-volume:focus-within .mp-volume-slider { width: 78px; opacity: 1; margin-right: var(--s-2); }

/* A range input is one of the few controls with no useful default styling
   in any browser, so both tracks and both thumbs are declared explicitly. */
.mp-volume-slider {
  appearance: none; -webkit-appearance: none;
  height: 4px; border-radius: var(--r-pill);
  background: var(--surface-3); cursor: pointer;
}
.mp-volume-slider::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none;
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--accent); border: 0; box-shadow: var(--elev-1);
}
.mp-volume-slider::-moz-range-thumb {
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--accent); border: 0;
}

/* The icon reflects the level. All three ship in the DOM and CSS picks, so
   the state is right before any script runs. */
.mp-mute .icon { display: none; }
.mini-player[data-volume="high"] .icon-vol-high,
.mini-player:not([data-volume]) .icon-vol-high { display: block; }
.mini-player[data-volume="low"] .icon-vol-low { display: block; }
.mini-player[data-volume="mute"] .icon-vol-mute { display: block; }

/* --- Favourite / share ---------------------------------------------------- */
.mp-fav .icon { transition: transform var(--t-base) var(--ease-spring), fill var(--t-fast) linear; }
.mp-fav.is-favorited { color: var(--text); }
/* Filled by CSS rather than by swapping to a second symbol, so the
   transition actually animates instead of cutting. */
.mp-fav.is-favorited .icon { fill: currentColor; transform: scale(1.12); }

.mp-share.is-copied { color: var(--success); }

.mp-close .icon { width: 16px; height: 16px; }

/* ==========================================================================
   FORMS & FILTERS
   ========================================================================== */
input[type="text"], input[type="search"], input[type="number"], input[type="email"],
input[type="password"], input[type="url"], select, textarea {
  width: 100%;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text);
  padding: 10px 13px;
  font-family: var(--font-body); font-size: 14px;
  transition: border-color var(--t-fast) var(--ease-out), box-shadow var(--t-fast) var(--ease-out), background-color var(--theme-ease);
}
input:hover, select:hover, textarea:hover { border-color: var(--border-bright); }
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  /* A soft ring instead of a hard 2px outline — same affordance, reads as
     a focus glow rather than a system artefact. */
  box-shadow: 0 0 0 3px var(--accent-wash);
}
input::placeholder, textarea::placeholder { color: var(--text-faint); }
textarea { min-height: 120px; resize: vertical; }
label { font-size: 13px; color: var(--text-dim); }

/* Native selects render their own arrow in the OS style, which is the one
   control that gives a custom UI away instantly. Suppressed and replaced
   with an inline chevron. */
select {
  appearance: none; -webkit-appearance: none;
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23a1a1aa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9.5 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
}

.field { display: flex; flex-direction: column; gap: 6px; }
.field-row { display: grid; gap: var(--s-4); }

/* Validation and help, for the shared _form_fields.html renderer. Django's
   default output is a <ul class="errorlist"> that nothing here styled, so a
   rejected form came back as an unstyled bulleted list. */
.field-required { color: var(--text-faint); margin-left: 3px; }
.field-help { font-size: 12px; color: var(--text-faint); }
.field-error { font-size: 12px; color: var(--danger); }
.field.has-error input,
.field.has-error select,
.field.has-error textarea { border-color: var(--danger); }
.field.has-error input:focus,
.field.has-error select:focus,
.field.has-error textarea:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 22%, transparent);
}
.form-errors {
  border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent);
  background: color-mix(in srgb, var(--danger) 10%, transparent);
  border-radius: var(--r-sm);
  padding: var(--s-3) var(--s-4);
  margin-bottom: var(--s-4);
}
.form-errors p { margin: 0; color: var(--text); font-size: 14px; }

/* A form standing alone on a page — contact, login, sign-up, checkout.
   Panelled and width-capped: a single column of inputs stretched to a
   1240px wrap reads as a database screen, and a 90-character-wide text
   input is harder to scan than a short one. */
.form-panel {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-1);
  padding: var(--s-6);
  max-width: 560px;
  display: flex; flex-direction: column; gap: var(--s-4);
}
/* A form nested inside the panel (checkout, pay) gets the same rhythm as
   one whose fields are direct children (contact, login). */
.form-panel form { display: flex; flex-direction: column; gap: var(--s-4); }
.form-panel .btn { align-self: flex-start; }
@media (max-width: 640px) {
  .form-panel { padding: var(--s-4); }
  .form-panel .btn { align-self: stretch; text-align: center; }
}

/* Key/value rows for an order summary. Dotted leader so the eye tracks
   from label to figure without a table's borders. */
.summary { display: flex; flex-direction: column; gap: var(--s-2); }
.summary-row { display: flex; align-items: baseline; gap: var(--s-3); font-size: 14px; }
.summary-row dt { color: var(--text-dim); flex: none; margin: 0; }
.summary-row dd {
  margin: 0 0 0 auto; color: var(--text); text-align: right;
  overflow-wrap: anywhere;
}
.summary-total { border-top: 1px solid var(--border); padding-top: var(--s-3); margin-top: var(--s-2); }
.summary-total dt, .summary-total dd { font-size: 17px; color: var(--text); }
.summary-total dd { font-weight: 600; }

/* Filter bar. Was an outlined box of stacked label+control pairs; now a
   single glass rail of pill controls. */
.filter-bar {
  background: var(--surface-2);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-1);
  padding: var(--s-4);
  margin-bottom: var(--s-5);
}
.filter-row { display: flex; gap: var(--s-3); flex-wrap: wrap; align-items: center; }
.filter-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.filter-field label {
  font-family: var(--font-label); font-weight: 550; font-size: 10px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--text-faint); padding-left: 2px;
}
.filter-field.grow { flex: 1 1 220px; }
.filter-field select, .filter-field input { border-radius: var(--r-pill); padding-left: 15px; }
.filter-field.narrow { width: 96px; flex: none; }
.filter-actions { display: flex; gap: var(--s-2); flex: none; }
.filter-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4); flex-wrap: wrap;
  margin-top: var(--s-4); padding-top: var(--s-4);
  border-top: 1px solid var(--border);
}
.filter-summary {
  margin: 0;
  font-family: var(--font-label); font-weight: 550; font-size: 11px; color: var(--text-faint);
  letter-spacing: .04em;
}

/* Search field with a leading icon. */
.input-icon { position: relative; display: flex; align-items: center; }
.input-icon .icon { position: absolute; left: 13px; width: 16px; height: 16px; color: var(--text-faint); pointer-events: none; }
.input-icon input { padding-left: 38px; }

/* The label is the hit area, not just the 13px box next to it. A native
   checkbox renders at 13x13 and a filter you cannot reliably tap is a
   filter nobody uses on a phone. */
.filter-check {
  display: inline-flex; align-items: center; gap: 10px;
  font-size: 13px; color: var(--text-dim); margin-top: var(--s-3);
  min-height: var(--tap-min); padding-right: var(--s-3);
  cursor: pointer;
}
.filter-check:hover { color: var(--text); }
.filter-check input { width: 18px; height: 18px; flex: none; accent-color: var(--accent); cursor: pointer; }

/* ==========================================================================
   TABLES
   ========================================================================== */
table.data { width: 100%; border-collapse: collapse; font-size: 14px; }
/* Cell padding is 16px vertical / 24px horizontal. A licence table is read
   across the row, and the columns need enough gutter that the eye does not
   have to re-find which price belongs to which tier. */
table.data th {
  text-align: left; padding: var(--s-4) var(--s-5);
  font-family: var(--font-label); font-size: 10px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-faint);
  border-bottom: 1px solid var(--border-bright);
}
table.data td { padding: var(--s-4) var(--s-5); border-bottom: 1px solid var(--border); vertical-align: middle; }
/* Edge cells pull back to the panel edge so the first column is not
   double-indented by its own padding plus the panel's. */
table.data th:first-child, table.data td:first-child { padding-left: var(--s-4); }
table.data th:last-child,  table.data td:last-child  { padding-right: var(--s-4); }
table.data tbody tr { transition: background-color var(--t-fast) linear; }
table.data tbody tr:hover { background: var(--surface-2); }
table.data tr:last-child td { border-bottom: 0; }
.table-scroll { overflow-x: auto; border-radius: var(--r-md); }

/* ==========================================================================
   DETAIL PAGES
   ========================================================================== */
.beat-detail { display: grid; grid-template-columns: 340px 1fr; gap: var(--s-8); align-items: start; }
.cover-large {
  border-radius: var(--r-lg); overflow: hidden;
  background: var(--surface-3); aspect-ratio: 1/1;
  box-shadow: var(--elev-2);
}
.cover-large img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The player on a detail page is a panel, not a bare row. */
.detail-player {
  background: var(--surface-2); border-radius: var(--r-lg);
  box-shadow: var(--elev-1); padding: var(--s-4) var(--s-5);
  margin: var(--s-5) 0;
}

.tag-list { list-style: none; display: flex; flex-wrap: wrap; gap: var(--s-2); padding: 0; margin: 0 0 var(--s-5); }

/* Sub-headings inside a detail column. Smaller than a page section title
   and much tighter above their content — a full --s-8 gap between "Mood"
   and its three chips reads as a page break, not a label. */
.detail-heading {
  font-size: 11px; font-weight: 600;
  font-family: var(--font-label); letter-spacing: .13em; text-transform: uppercase;
  color: var(--text-faint);
  margin: var(--s-6) 0 var(--s-3);
}

/* Spec cells as soft-3D tiles — the mixing-console readout motif. */
.spec-list {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  gap: var(--s-3); margin: var(--s-5) 0; padding: 0; list-style: none;
}
.spec-item {
  background: var(--tile); border-radius: var(--r-md);
  box-shadow: var(--tile-edge); padding: var(--s-3) var(--s-4);
}
.spec-item dt {
  font-family: var(--font-label); font-size: 10px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-faint);
  margin-bottom: 4px;
}
.spec-item dd { margin: 0; font-family: var(--font-numeric); font-size: 15px; color: var(--text); }

/* ==========================================================================
   HOMEPAGE SECTIONS
   ========================================================================== */
.about-inner { display: flex; gap: var(--s-7); align-items: flex-start; flex-wrap: wrap; }
.producer-photo {
  width: 240px; height: 240px; object-fit: cover;
  border-radius: var(--r-lg); box-shadow: var(--elev-2);
}
.about-copy { flex: 1; min-width: 280px; }

.testimonial-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--s-5); }
.testimonial {
  margin: 0; padding: var(--s-5);
  background: var(--surface-2); border-radius: var(--r-lg); box-shadow: var(--elev-1);
}
.testimonial blockquote { margin: 0 0 var(--s-4); font-size: 15px; line-height: 1.7; color: var(--text-soft); }
.testimonial blockquote::before {
  content: "\201C"; display: block;
  font-family: var(--font-display); font-size: 40px; line-height: .6;
  color: var(--text-faint); margin-bottom: var(--s-2);
}
.testimonial figcaption { display: flex; align-items: center; gap: var(--s-3); font-size: 13px; font-weight: 500; }
.testimonial figcaption img { width: 38px; height: 38px; object-fit: cover; border-radius: 50%; }
.testimonial figcaption em { display: block; color: var(--text-faint); font-style: normal; font-size: 11px; font-weight: 400; }

.faq-list { display: grid; gap: var(--s-2); }
.faq-item {
  background: var(--surface-2); border-radius: var(--r-md);
  padding: var(--s-4) var(--s-5);
  transition: background-color var(--t-fast) var(--ease-out);
}
.faq-item:hover { background: var(--surface-3); }
.faq-item summary {
  cursor: pointer; font-weight: 550; list-style: none;
  display: flex; align-items: center; gap: var(--s-3);
  /* The summary IS the control. At its natural 25px it was a smaller tap
     target than any button on the site while being the only thing on the
     FAQ page you can press. */
  min-height: var(--tap-min);
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: ""; margin-left: auto; flex: none;
  width: 9px; height: 9px;
  border-right: 1.75px solid var(--text-faint); border-bottom: 1.75px solid var(--text-faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--t-base) var(--ease-out);
}
.faq-item[open] summary::after { transform: rotate(-135deg) translate(-3px, -3px); }
.faq-answer { color: var(--text-dim); padding-top: var(--s-3); font-size: 14px; }

/* -- The FAQ page itself (the homepage block above stays compact) -------
   Larger type, a numeral per question, and a hairline between entries
   instead of a gap, so the list reads as one index rather than as four
   detached cards. */
.faq-list-lg { gap: 0; }
.faq-list-lg .faq-item {
  background: none; border-radius: 0;
  border-bottom: 1px solid var(--border);
  padding: var(--s-2) 0;
}
.faq-list-lg .faq-item:first-child { border-top: 1px solid var(--border); }
.faq-list-lg .faq-item:hover { background: none; }
.faq-list-lg .faq-item:hover .faq-q { color: var(--accent-bright); }
.faq-list-lg summary {
  min-height: 68px; gap: var(--s-4);
  font-size: clamp(15px, 1.6vw, 18px); font-weight: 600;
  letter-spacing: -0.018em;
}
.faq-index {
  font-family: var(--font-numeric); font-size: 11px; font-weight: 500;
  letter-spacing: .1em; color: var(--text-faint);
  flex: none; padding-top: 2px;
}
.faq-q { transition: color var(--t-fast) var(--ease-out); }
.faq-list-lg .faq-answer {
  font-size: 15px; line-height: 1.75; color: var(--text-soft);
  /* Aligned under the question, not under the numeral: the index column is
     11px mono + a 16px gap. */
  padding: 0 var(--s-8) var(--s-5) calc(20px + var(--s-4));
  max-width: 74ch;
}

/* The last thing on the page, not a banner over it. */
.support-panel {
  margin-top: var(--section-gap);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-5); flex-wrap: wrap;
  padding: var(--s-6) var(--s-6);
  background: var(--surface-2); border-radius: var(--r-xl);
  box-shadow: var(--elev-1);
}
.support-panel h2 { font-size: 22px; margin-bottom: var(--s-1); }
.support-panel p { color: var(--text-dim); margin: 0; max-width: 46ch; }

/* ==========================================================================
   EDITORIAL PAGES  (About, FAQ)
   A masthead that states one thing, then bands of content with their own
   vertical padding. The rhythm has to survive any band being absent —
   no photo, no stats and no socials are all normal states for a fresh
   install — so the spacing lives on the bands, never between them.
   ========================================================================== */
.editorial-masthead { padding-bottom: var(--s-7); }
.editorial-masthead h1 {
  font-size: clamp(44px, 8.5vw, 108px);
  line-height: .92;
  letter-spacing: -0.045em;
  margin: 0 0 var(--s-5);
  text-wrap: initial;      /* the line break is authored, not balanced */
}
/* The one all-caps headline on the site. Caps need looser tracking than
   the -0.045em that suits mixed case, or the letters collide. */
.masthead-title { letter-spacing: -0.02em; }
.masthead-tight h1 { font-size: clamp(38px, 6.5vw, 76px); }
.editorial-standfirst {
  font-size: clamp(16px, 1.7vw, 20px); line-height: 1.5;
  color: var(--text-dim); max-width: 46ch; margin: 0;
}

.editorial-band { padding-block: var(--section-gap); }
.editorial-band + .editorial-band { padding-top: 0; }
.editorial-band .section-head:first-child { margin-top: 0; }

/* -- Centred variant ---------------------------------------------------
   One axis, every band. A centred column at 390px and a centred column at
   1440px are the same layout, which is why this needs no mobile override:
   the only thing that changes with width is the measure. */
.is-centred { text-align: center; }
.is-centred .editorial-masthead,
.is-centred .section-head { text-align: center; }
.is-centred .editorial-standfirst,
.is-centred .section-sub { margin-inline: auto; }
.is-centred .hero-actions { justify-content: center; }
.is-centred .social-rail { justify-content: center; }

/* The portrait leads the page. Round, because that is what a profile photo
   is, and bordered rather than shadowed so it survives a light background
   as well as a dark one. */
.portrait-round {
  width: clamp(132px, 16vw, 190px);
  aspect-ratio: 1;
  margin: 0 auto var(--s-6);
  border-radius: 50%;
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--border-bright);
  box-shadow: var(--elev-2);
}
.portrait-round img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* No photo uploaded. A monogram, not an empty circle: a blank ring at the
   top of the page reads as a broken image, a lettermark reads as a choice. */
.portrait-round.is-monogram {
  display: grid; place-items: center;
  background: var(--tile);
  box-shadow: var(--tile-edge);
}
.portrait-round.is-monogram span {
  font-family: var(--font-display);
  font-size: clamp(48px, 6vw, 72px); font-weight: 600;
  letter-spacing: -0.04em; line-height: 1;
  color: var(--text-dim);
}

/* The bio is the reason the page exists, so it gets the widest type on it
   and a measure that stays readable — centred text past ~62 characters is
   hard to track back to the start of the next line. */
.about-bio {
  max-width: 62ch;
  margin-inline: auto;
}
.about-bio p {
  font-size: clamp(16px, 1.5vw, 18px);
  line-height: 1.75;
  color: var(--text-soft);
}
.about-bio .hero-actions { margin-top: var(--s-6); }

/* Numbers, as a mixing-console readout. Border-collapsed hairlines rather
   than card gaps so it reads as one instrument, not four tiles. */
.stat-strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px; margin: 0; padding: 0;
  background: var(--border); border: 1px solid var(--border);
  border-radius: var(--r-lg); overflow: hidden;
}
.stat-cell { background: var(--surface-1); padding: var(--s-5) var(--s-5); }
.stat-cell dt {
  font-family: var(--font-label); font-size: 10px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--text-faint);
  margin-bottom: var(--s-2);
}
.stat-cell dd {
  margin: 0; font-family: var(--font-display);
  font-size: clamp(30px, 3.6vw, 44px); font-weight: 600;
  letter-spacing: -0.035em; line-height: 1;
  font-feature-settings: "tnum" 1;
}

.craft-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--s-4); }
.craft-card {
  background: var(--tile); border-radius: var(--r-lg);
  box-shadow: var(--tile-edge);
  padding: var(--s-6) var(--s-5) var(--s-5);
}
.craft-index {
  display: block; font-family: var(--font-numeric); font-size: 11px;
  letter-spacing: .16em; color: var(--text-faint); margin-bottom: var(--s-5);
}
.craft-card h3 { font-size: 19px; margin-bottom: var(--s-3); letter-spacing: -0.024em; }
.craft-card p { color: var(--text-dim); font-size: 14px; line-height: 1.7; margin: 0; }

/* The social rail is 40px in the footer; on a page of its own it earns 52. */
.social-rail-lg .social-link { width: 52px; height: 52px; }
.social-rail-lg .social-link .icon { width: 22px; height: 22px; }

.cta-block {
  position: relative; overflow: hidden;
  background: var(--surface-2); border-radius: var(--r-xl);
  box-shadow: var(--elev-2);
  padding: var(--s-8) var(--s-5); text-align: center; margin: var(--s-8) 0;
}
/* A single soft light source in the corner. The only "decoration" in the
   whole stylesheet, and it is a radial gradient of the text colour at 5% —
   monochrome, and invisible if the palette changes. */
.cta-block::before {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(70% 120% at 50% -20%, var(--accent-wash), transparent 70%);
  pointer-events: none;
}
.cta-block > * { position: relative; }
.cta-block h2 { font-size: clamp(26px, 3.4vw, 38px); }
.cta-block .hero-actions { justify-content: center; margin-top: var(--s-5); }

.deal-banner { list-style: none; margin: 0 0 var(--s-5); padding: 0; display: flex; flex-wrap: wrap; gap: var(--s-2); }
.deal-banner li {
  padding: 7px 14px; border-radius: var(--r-pill);
  background: var(--accent-wash); color: var(--text-soft);
  font-family: var(--font-label); font-size: 12px; font-weight: 550;
  letter-spacing: .01em; text-transform: none;
}
.deal-banner-lg { display: grid; gap: var(--s-2); }
.deal-banner-lg li {
  display: block; padding: var(--s-4) var(--s-5); border-radius: var(--r-md);
  background: var(--surface-2); font-family: var(--font-body); font-size: 14px;
  text-transform: none; letter-spacing: 0; color: var(--text);
}
.deal-name { font-weight: 600; }
.deal-scope { color: var(--text-dim); margin-left: var(--s-2); }
.deal-applied { color: var(--text); font-size: 13px; }
.deal-nudge { color: var(--text-dim); font-size: 13px; }

/* ==========================================================================
   COURSES
   ========================================================================== */
.progress { height: 6px; background: var(--surface-3); border-radius: var(--r-pill); overflow: hidden; margin: var(--s-2) 0; }
.progress-fill { display: block; height: 100%; background: var(--accent); border-radius: var(--r-pill); transition: width var(--t-base) var(--ease-out); }
.progress-label { font-family: var(--font-label); font-weight: 550; font-size: 11px; color: var(--text-dim); margin: 0; }

.course-list { display: grid; gap: var(--s-3); }
.course-row {
  display: flex; gap: var(--s-5); align-items: flex-start;
  background: var(--surface-2); border-radius: var(--r-lg); padding: var(--s-4);
  box-shadow: var(--elev-1);
  transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base) var(--ease-out);
}
.course-row:hover { transform: translateY(-2px); box-shadow: var(--elev-2); }
.course-row-cover { flex: none; }
.course-row-cover img { width: 132px; height: 132px; object-fit: cover; display: block; border-radius: var(--r-md); }
.course-row-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--s-2); }

.module-block { margin-bottom: var(--s-5); }
.module-heading {
  font-family: var(--font-label); font-weight: 550; font-size: 10px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--text-faint); margin: var(--s-5) 0 var(--s-2);
}

.lesson-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.lesson-item {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-4); border-radius: var(--r-sm);
  transition: background-color var(--t-fast) linear;
}
.lesson-item:hover { background: var(--surface-2); }
.lesson-state { flex: none; width: 18px; text-align: center; font-size: 12px; color: var(--text-faint); }
.lesson-item.is-done .lesson-state { color: var(--success); }
.lesson-item.is-locked { color: var(--text-faint); }
.lesson-item.is-locked:hover { background: none; }
.lesson-name { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.lesson-length { flex: none; font-family: var(--font-numeric); font-size: 11px; color: var(--text-faint); }

.lesson-video { margin: 0 0 var(--s-5); border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--elev-2); }
.lesson-video video { width: 100%; display: block; background: #000; aspect-ratio: 16/9; }
.hls-fallback { margin-top: var(--s-2); font-size: 12px; }

.lesson-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-3); flex-wrap: wrap;
  background: var(--surface-2); border-radius: var(--r-md);
  padding: var(--s-3) var(--s-4); margin: var(--s-5) 0;
}
.lesson-toolbar #complete-toggle { display: flex; align-items: center; gap: var(--s-3); }
.lesson-nav { display: flex; gap: var(--s-2); }
.lesson-body { color: var(--text-soft); }

.resource-list { list-style: none; margin: 0 0 var(--s-5); padding: 0; display: grid; gap: 2px; }
.resource-list li {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-4); border-radius: var(--r-sm);
  background: var(--surface-2);
}
.curriculum-preview { background: var(--surface-2); border-radius: var(--r-lg); padding: var(--s-3) var(--s-5); }

/* ==========================================================================
   COMMERCE
   ========================================================================== */
.detail-heading-note {
  margin: calc(var(--s-3) * -1) 0 var(--s-4); font-size: 13.5px; color: var(--text-dim);
}

.license-table .license-name { font-weight: 600; }
.license-delivers { font-size: 13px; color: var(--text-dim); }
.license-actions { display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap; }
.license-actions form { margin: 0; }

.product-buy { background: var(--surface-2); border-radius: var(--r-lg); box-shadow: var(--elev-1); padding: var(--s-5); margin: var(--s-5) 0; }
.product-price {
  font-family: var(--font-display);
  font-size: clamp(28px, 3vw, 36px); font-weight: 600; letter-spacing: -0.035em;
  margin: 0 0 var(--s-4); font-feature-settings: "tnum" 1;
}
.product-price strong { font-size: 30px; font-weight: 500; letter-spacing: -0.02em; }
.product-price s { color: var(--text-faint); margin-right: var(--s-2); font-size: 15px; }
.product-gallery { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-2); margin-top: var(--s-3); }
.product-gallery img { width: 100%; height: auto; display: block; border-radius: var(--r-sm); }
.product-description { color: var(--text-soft); }

.cart-table .line-free td { color: var(--text-dim); }
.cart-table .line-free strong { color: var(--text); }
.totals { max-width: 360px; margin-left: auto; }
.totals td { padding: var(--s-2) var(--s-4); }
.totals .grand td { border-top: 1px solid var(--border-bright); font-size: 18px; font-weight: 600; padding-top: var(--s-4); }
.checkout-form { max-width: 360px; margin-left: auto; text-align: right; }
.checkout-form input { margin-bottom: var(--s-3); }
.order-lines { list-style: none; margin: 0; padding: 0; font-size: 13px; }

/* --- Downloads ------------------------------------------------------------
   A deliverable can arrive in pieces (Cloudflare refuses any upload over
   100MB), and the pieces are worthless individually. So a split set is
   drawn as one bounded group with an accent edge — three links in a flat
   list look interchangeable, and a buyer who takes one and stops has a
   folder that will not open. */
.dl-split-note { margin-bottom: var(--s-6); }
.dl-split-note p { margin: 0 0 var(--s-1); }
.dl-split-note p:last-child { margin: 0; }

.dl-order { margin-bottom: var(--s-7); }
.dl-order-head {
  display: flex; align-items: baseline; gap: var(--s-3);
  padding-bottom: var(--s-2); border-bottom: 1px solid var(--border);
  margin-bottom: var(--s-4);
}
.dl-order-head h2 { margin: 0; font-size: 15px; font-family: var(--font-numeric); }

.dl-item { margin-bottom: var(--s-5); }
.dl-item h3 { margin: 0 0 var(--s-2); font-size: 15px; }
.dl-item.has-parts {
  padding: var(--s-4); border-radius: var(--r-md);
  background: var(--surface-2); border-left: 2px solid var(--accent);
}

.dl-files { list-style: none; margin: 0; padding: 0; }
.dl-files li { padding: 5px 0; }
.dl-files a { font-size: 14px; }
.dl-parts-hint { margin: var(--s-2) 0 0; font-size: 12px; }

/* 404 / 403. Centred in whatever height the sticky-footer column leaves,
   so the message sits in the middle of the page rather than jammed under
   the header with the footer floating below it. */
.error-page {
  max-width: 560px; margin: 0 auto; padding: var(--s-9) 0;
  text-align: center;
}
.error-page .hero-actions { justify-content: center; margin-top: var(--s-5); }
.error-page .lead { margin-bottom: 0; }

.pagination { display: flex; align-items: center; justify-content: center; gap: var(--s-4); margin: var(--s-8) 0 0; }
.pagination .page-info { font-family: var(--font-numeric); font-size: 11px; color: var(--text-faint); letter-spacing: .06em; }

/* ==========================================================================
   LICENSING PAGE
   A pricing page has one job: let somebody read ACROSS a row and see what
   the extra money buys. Every decision below serves that — the sticky row
   label, the horizontal scroll that is deliberately not a card stack, the
   featured column that is tinted rather than enlarged.
   ========================================================================== */
.lic-hero { max-width: 62ch; margin: var(--s-8) 0 var(--s-8); }
.lic-hero h1 { margin: 0 0 var(--s-3); }
.lic-lede { color: var(--text-soft); font-size: 16px; line-height: 1.65; margin: 0; }

/* auto-fit rather than a fixed four: a shop with three tiers should not
   render a gap where the fourth would have been. */
.lic-tiers {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--s-4); margin-bottom: var(--s-9);
}
.lic-tier {
  position: relative; display: flex; flex-direction: column;
  padding: var(--s-5); border-radius: var(--r-lg);
  background: var(--surface-2); border: 1px solid var(--border);
  transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-base) var(--ease-out);
}
.lic-tier:hover { transform: translateY(-3px); box-shadow: var(--elev-2); }

/* The recommended tier is marked with a border and a flag, NOT by being
   scaled up. A card that is physically larger than its neighbours breaks
   the horizontal alignment that makes the group readable as a set. */
.lic-tier.is-featured { border-color: var(--accent); background: var(--surface-3); }
.lic-tier-flag {
  position: absolute; top: calc(var(--s-3) * -1); left: var(--s-5);
  padding: 3px 10px; border-radius: var(--r-pill);
  background: var(--accent); color: var(--accent-ink);
  font-family: var(--font-label); font-size: 10px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
}

.lic-tier-name { margin: 0; font-size: 17px; letter-spacing: -.01em; }
.lic-tier-tagline {
  margin: var(--s-1) 0 0; font-size: 13px; color: var(--text-dim); min-height: 2.6em;
}
.lic-tier-price {
  margin: var(--s-4) 0 var(--s-4); font-family: var(--font-numeric);
  font-feature-settings: "tnum" 1;
}
.lic-tier-price strong {
  font-family: var(--font-display); font-size: 30px; font-weight: 600; letter-spacing: -.03em;
}
.lic-tier-price s { color: var(--text-faint); font-size: 14px; margin-right: 6px; }
.lic-tier-per { display: block; font-size: 11px; color: var(--text-faint); letter-spacing: .04em; }

.lic-tier-points { list-style: none; margin: 0 0 var(--s-5); padding: 0; flex: 1; }
.lic-tier-points li {
  position: relative; padding-left: 20px; margin-bottom: var(--s-2);
  font-size: 13px; color: var(--text-soft); line-height: 1.5;
}
.lic-tier-points li::before {
  content: ""; position: absolute; left: 3px; top: .55em;
  width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
}
.lic-tier-cta { width: 100%; justify-content: center; }

/* --- the table ---------------------------------------------------------- */

.lic-compare { margin-bottom: var(--s-9); }
.lic-compare h2 { margin: 0 0 var(--s-2); }
.lic-compare-hint { font-size: 12px; color: var(--text-faint); margin: 0 0 var(--s-4); }

/* Its own scroll container, so the PAGE never scrolls sideways. The
   alternative — stacking each row into a card per tier — destroys the one
   thing a comparison table is for. */
.lic-table-scroll {
  overflow-x: auto; border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--surface-1);
}
.lic-table-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.lic-table { width: 100%; border-collapse: collapse; min-width: 640px; }
.lic-table th, .lic-table td {
  padding: var(--s-3) var(--s-4); text-align: left; vertical-align: top;
  border-bottom: 1px solid var(--border);
}
.lic-table tbody tr:last-child th, .lic-table tbody tr:last-child td { border-bottom: 0; }
.lic-table thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--surface-2); border-bottom: 1px solid var(--border-bright);
  white-space: nowrap;
}
.lic-table thead th.is-featured { background: var(--accent-wash); }
.lic-th-name { display: block; font-size: 14px; }
.lic-th-price {
  display: block; margin-top: 2px;
  font-family: var(--font-numeric); font-feature-settings: "tnum" 1;
  font-size: 12px; color: var(--text-dim);
}

/* The row label stays put while the tier columns scroll under it —
   without this, three columns along, the reader no longer knows which
   row they are looking at. */
.lic-table tbody th {
  position: sticky; left: 0; z-index: 1;
  background: var(--surface-1); min-width: 190px;
  font-weight: 500; font-size: 14px;
}
.lic-row-note {
  display: block; margin-top: 3px; font-size: 11.5px; line-height: 1.45;
  color: var(--text-faint); font-weight: 400; max-width: 34ch;
}

.lic-table td { font-size: 14px; }
.lic-yes { width: 17px; height: 17px; color: var(--success); }
.lic-no { color: var(--text-faint); }
.lic-value { font-family: var(--font-numeric); font-feature-settings: "tnum" 1; }

/* --- supporting copy ---------------------------------------------------- */

.lic-products { margin-bottom: var(--s-9); }
.lic-deflist { margin: var(--s-4) 0 0; }
.lic-deflist > div { padding: var(--s-3) 0; border-bottom: 1px solid var(--border); }
.lic-deflist dt { font-weight: 600; margin: 0 0 3px; }
.lic-deflist dd { margin: 0; color: var(--text-soft); font-size: 14px; }

.lic-notes { margin-bottom: var(--s-9); }
.lic-note-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--s-5); margin-top: var(--s-5);
}
.lic-note-grid h3 { margin: 0 0 var(--s-2); font-size: 15px; }
.lic-note-grid p { margin: 0; font-size: 14px; color: var(--text-soft); line-height: 1.6; }

.lic-cta {
  text-align: center; padding: var(--s-8) var(--s-5); margin-bottom: var(--s-8);
  background: var(--surface-2); border-radius: var(--r-lg);
}
.lic-cta h2 { margin: 0 0 var(--s-2); }
.lic-cta p { color: var(--text-soft); margin: 0 0 var(--s-5); }
.lic-cta .btn + .btn { margin-left: var(--s-2); }

@media (max-width: 560px) {
  /* The note under each row label is the first thing to go — at this width
     it doubles the height of every row and pushes the tier columns off
     screen before the reader has seen one. */
  .lic-row-note { display: none; }
  .lic-table tbody th { min-width: 140px; }
  .lic-tier-tagline { min-height: 0; }
  .lic-cta .btn { display: block; width: 100%; }
  .lic-cta .btn + .btn { margin: var(--s-2) 0 0; }
}

/* ==========================================================================
   CHECKOUT
   Nine billing fields between a customer and a purchase. Everything here
   is about keeping the total on screen and the form quick to fill.
   ========================================================================== */
.checkout-layout {
  display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(260px, 1fr);
  gap: var(--s-6); align-items: start;
}
.checkout-summary {
  padding: var(--s-5); border-radius: var(--r-lg);
  background: var(--surface-2); border: 1px solid var(--border);
  position: sticky; top: var(--s-5);
}
.summary-sub { display: block; font-size: 12px; color: var(--text-dim); margin-top: 2px; }
.checkout-fx {
  margin: var(--s-4) 0 0; padding-top: var(--s-4);
  border-top: 1px solid var(--border);
  font-size: 12px; color: var(--text-dim); line-height: 1.6;
}
/* Every "back to …" link on the site.
   Four templates wrote this four different ways — .meta, .checkout-back and
   a bare <p> — so it sat at a different distance from the panel above it on
   each page, and on the pay page the browser default left it visibly
   cramped against the box. One class, one spacing. */
.back-link {
  margin: var(--s-6) 0 0; font-size: 13px;
}
.back-link a { color: var(--text-dim); text-underline-offset: 3px; }
.back-link a:hover { color: var(--text); }

.billing { border: 0; padding: 0; margin: 0 0 var(--s-5); }
.billing-legend {
  padding: 0; font-family: var(--font-label); font-size: 11px; font-weight: 650;
  letter-spacing: .1em; text-transform: uppercase; color: var(--text-dim);
}
/* The reason, said on the page. Nine fields on a shop that delivers a
   download to an email address reads as collection for its own sake
   otherwise, and a customer who does not know why closes the tab. */
.billing-why {
  margin: var(--s-2) 0 var(--s-4); font-size: 12.5px; color: var(--text-dim);
  line-height: 1.6;
}
.billing-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--s-3) var(--s-4);
}
.billing-grid .field-wide { grid-column: 1 / -1; }
.field-error { margin: 4px 0 0; font-size: 12px; color: var(--danger); }
.field.has-error input, .field.has-error select { border-color: var(--danger); }

@media (max-width: 860px) {
  .checkout-layout { grid-template-columns: 1fr; }
  /* Unstuck once it is above the form rather than beside it — a sticky
     block at the top of a scrolling column covers the fields. */
  .checkout-summary { position: static; order: -1; }
}

/* ==========================================================================
   CONTACT
   Two readers: a customer who wants to know they will get an answer, and a
   payment provider checking that support exists at all. Both are served by
   the same thing — channels stated up front with a time attached, rather
   than a form on its own.
   ========================================================================== */
.contact-channels {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--s-4); margin-bottom: var(--s-4);
}
.contact-channel {
  display: flex; gap: var(--s-4); align-items: flex-start;
  padding: var(--s-5); border-radius: var(--r-lg);
  background: var(--surface-2); border: 1px solid var(--border);
  color: inherit; text-decoration: none;
  transition: transform var(--t-base) var(--ease-out),
              border-color var(--t-fast) var(--ease-out);
}
.contact-channel:hover { transform: translateY(-2px); border-color: var(--border-bright); }
.contact-channel-icon {
  flex: none; width: 40px; height: 40px; border-radius: var(--r-md);
  display: grid; place-items: center;
  background: var(--accent-wash); color: var(--accent);
}
.contact-channel-icon .icon { width: 19px; height: 19px; }
.contact-channel-body { display: flex; flex-direction: column; min-width: 0; }
.contact-channel-label {
  font-family: var(--font-label); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-faint);
}
.contact-channel-value {
  font-size: 15px; font-weight: 550; color: var(--text); margin-top: 2px;
  /* An email address is the one string here long enough to overflow a
     narrow card, and it must not push the layout sideways. */
  overflow-wrap: anywhere;
}
.contact-channel-note {
  font-size: 12.5px; color: var(--text-dim); margin-top: var(--s-2); line-height: 1.5;
}

/* The commitment. Worth its own line rather than being buried in a card —
   it is the single sentence a reviewer is looking for. */
.contact-sla {
  margin: 0 0 var(--s-8); font-size: 13px; color: var(--text-dim);
}

.contact-layout {
  display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: var(--s-7); align-items: start;
}
.contact-h2 {
  margin: 0 0 var(--s-4);
  font-family: var(--font-label); font-size: 11px; font-weight: 650;
  letter-spacing: .1em; text-transform: uppercase; color: var(--text-dim);
}
.contact-aside { padding-top: 2px; }
.contact-links { list-style: none; margin: 0 0 var(--s-7); padding: 0; }
.contact-links li {
  padding: var(--s-3) 0; border-bottom: 1px solid var(--border);
}
.contact-links li:first-child { padding-top: 0; }
.contact-links a { display: block; font-size: 14px; font-weight: 550; }
.contact-links span {
  display: block; margin-top: 3px; font-size: 12.5px; color: var(--text-dim);
  line-height: 1.5;
}
.contact-note { margin: 0; font-size: 13px; color: var(--text-dim); line-height: 1.65; }

@media (max-width: 820px) {
  /* The form goes first on a phone: somebody who opened this page on a
     small screen came to write, not to browse links. */
  .contact-layout { grid-template-columns: 1fr; gap: var(--s-7); }
}

/* ==========================================================================
   LEGAL PAGES
   Terms, privacy, refunds. Read start to finish by roughly nobody and
   scanned for one clause by everybody, so the numbered headings have to
   stand out from the body far more than a heading normally would.
   ========================================================================== */
.legal-page { max-width: 68ch; margin: var(--s-8) 0 var(--s-9); }
.legal-page h1 { margin: 0 0 var(--s-2); }
.legal-updated {
  margin: 0 0 var(--s-7); font-size: 12px; color: var(--text-faint);
  letter-spacing: .04em;
}

.legal-body { color: var(--text-soft); line-height: 1.75; }
.legal-body p { margin: 0 0 var(--s-4); }

/* The scan target. Small caps rather than large type: the reader is
   looking for "9. WHAT IS PUBLIC", and a label reads faster at a glance
   than a headline does. */
.legal-body h2 {
  margin: var(--s-7) 0 var(--s-3);
  font-family: var(--font-label); font-size: 12px; font-weight: 650;
  letter-spacing: .1em; text-transform: uppercase; color: var(--text);
  padding-bottom: var(--s-2); border-bottom: 1px solid var(--border);
}
.legal-body h2:first-child { margin-top: 0; }

.legal-body ul { margin: 0 0 var(--s-4); padding-left: var(--s-5); }
.legal-body li { margin-bottom: var(--s-2); }
.legal-body li::marker { color: var(--accent); }

/* Emphasis in a legal document is doing real work — it marks the sentence
   somebody needs to find. Full text colour against the dimmer body is
   what makes it visible at a scan. */
.legal-body strong { color: var(--text); font-weight: 600; }
.legal-body a { color: var(--text); text-underline-offset: 2px; }

/* ==========================================================================
   FOOTER
   `margin-top: auto` is the second half of the sticky-footer mechanism:
   even when #page is short, the footer is pushed to the bottom of the flex
   column instead of sitting under the content.
   ========================================================================== */
.site-footer {
  margin-top: auto;
  border-top: 1px solid var(--border);
  background: var(--surface-1);
  padding: var(--s-8) 0 var(--s-5);
  color: var(--text-dim); font-size: 13px;
}
.footer-top {
  display: flex; gap: var(--s-7); flex-wrap: wrap;
  justify-content: space-between; align-items: flex-start;
  padding-bottom: var(--s-6);
}
.footer-brand { max-width: 320px; }
.footer-brand img { height: 30px; width: auto; display: block; margin-bottom: var(--s-4); }
.footer-text { margin: 0 0 var(--s-4); color: var(--text-dim); }

.footer-nav { display: flex; flex-direction: column; gap: 2px; }
/* Padded rather than gapped: a 13px link is a 21px tap target, which is
   under the WCAG 2.5.8 minimum of 24 and well under what a thumb hits at
   the very bottom of a phone screen. The padding is the target; the gap
   would only push them apart without making any of them bigger. */
.footer-nav a {
  color: var(--text-dim); font-size: 13px;
  display: inline-flex; align-items: center; min-height: 32px;
  padding: 2px 0;
}
.footer-nav a:hover { color: var(--text); }

.footer-bottom {
  display: flex; gap: var(--s-4); flex-wrap: wrap;
  justify-content: space-between; align-items: center;
  padding-top: var(--s-5); border-top: 1px solid var(--border);
}
.copyright { margin: 0; color: var(--text-faint); font-size: 12px; }

/* Legal row. Quiet by design — required links, not navigation — but a real
   underline on hover, because "quiet" and "looks like plain text" are not
   the same thing and a link nobody can see is one that is not really
   offered. */
.footer-legal {
  display: flex; flex-wrap: wrap; gap: var(--s-4);
  padding-top: var(--s-5); margin-top: var(--s-5);
  border-top: 1px solid var(--border);
}
.footer-legal a {
  color: var(--text-faint); font-size: 12px; text-decoration: none;
}
.footer-legal a:hover { color: var(--text-soft); text-decoration: underline; }
/* The bottom row now sits under this one, so its own rule would draw a
   second line immediately below the first. */
.footer-legal + .footer-bottom { border-top: 0; padding-top: var(--s-4); margin-top: 0; }

/* Social rail — soft-3D tiles. The links have existed in Admin since Phase
   1 and were never rendered anywhere; this is where they surface. Only the
   ones actually filled in are emitted, so there are no dead icons. */
/* It is a <ul>, so the list styling has to be reset explicitly — without
   this every icon renders with a bullet beside it. */
.social-rail { display: flex; gap: var(--s-2); flex-wrap: wrap; list-style: none; margin: 0; padding: 0; }
.social-link {
  width: 38px; height: 38px; border-radius: var(--r-md);
  display: grid; place-items: center;
  background: var(--tile); box-shadow: var(--tile-edge), var(--elev-1);
  color: var(--text-dim);
  transition: transform var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.social-link:hover { transform: translateY(-2px); color: var(--text); }
.social-link .icon { width: 17px; height: 17px; }

/* ==========================================================================
   MOTION
   ========================================================================== */
.reveal { opacity: 0; transform: translateY(16px); transition: opacity 620ms var(--ease-out), transform 620ms var(--ease-out); }
.reveal.is-in { opacity: 1; transform: none; }

/* STAGGER. Grid children rise in sequence rather than as one slab, which
   is what makes a catalogue feel like it is being dealt rather than
   switched on.

   `--i` is set by player.js and CAPPED there, deliberately: an uncapped
   index on a 40-card page would make the last card wait 1.8s after the
   first, so a visitor who scrolls fast watches an empty grid fill in
   behind them. Capped, the cascade always finishes in about half a
   second no matter how many cards there are.

   Delay applies only on the way IN. Removing it from the resting state
   would make a theme switch or any other transition on these elements
   inherit the stagger too. */
.stagger > * {
  opacity: 0; transform: translateY(14px);
  transition: opacity 520ms var(--ease-out), transform 520ms var(--ease-out);
}
.stagger.is-in > * {
  opacity: 1; transform: none;
  transition-delay: calc(var(--i, 0) * 42ms);
}

/* The heart, when it fills. A single pulse — the state change is already
   carried by the fill, so this only has to confirm the tap landed. */
@keyframes fav-pop {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.32); }
  100% { transform: scale(1.12); }
}
.mp-fav.is-favorited .icon { animation: fav-pop 340ms var(--ease-spring); }

/* Nav underline. It used to appear at full width instantly, which read as
   a repaint rather than as a transition. */
.main-nav a[aria-current="page"]::after {
  animation: nav-underline 300ms var(--ease-out);
  transform-origin: center;
}
@keyframes nav-underline {
  from { transform: scaleX(0); opacity: 0; }
  to   { transform: scaleX(1); opacity: 1; }
}

/* Boosted navigation. The outgoing page dims (see .htmx-request below) and
   the incoming one rises the last few pixels, so a swap reads as a move
   rather than as a flash of new content. Transform and opacity only —
   both compositor-driven, so this costs nothing on a long page. */
@keyframes page-enter {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
#page.is-entering { animation: page-enter 260ms var(--ease-out); }

/* HTMX in-flight feedback — the only signal that a boosted navigation is
   happening. Opacity only, so nothing reflows mid-request. */
.htmx-request #page, #page.htmx-request { opacity: .5; transition: opacity 120ms linear; }

/* ==========================================================================
   RESPONSIVE
   ========================================================================== */
@media (max-width: 1000px) {
  .beat-detail { grid-template-columns: 280px 1fr; gap: var(--s-6); }
}

@media (max-width: 860px) {
  .beat-detail { grid-template-columns: 1fr; }
  .cover-large { max-width: 340px; }
  .footer-top { gap: var(--s-6); }
}

@media (max-width: 820px) {
  .nav-toggle { display: inline-grid; place-items: center; }

  /* The collapsed nav is a glass sheet under the header, not a bare list.
     It is `position: fixed` so it never scrolls away from its trigger. */
  .main-nav {
    display: none;
    position: fixed; left: var(--s-3); right: var(--s-3);
    top: calc(var(--header-height) + var(--s-2));
    flex-direction: column; align-items: stretch; gap: 2px;
    padding: var(--s-3);
    border-radius: var(--r-lg);
    /* Opaque, NOT glass.
       This was --glass-strong: rgba(24,24,27,.74) plus a 28px backdrop
       blur. Over most of the site that reads as a frosted sheet, but the
       menu opens over the hero — 56px display type on video — and neither
       74% coverage nor a blur can hide that. "SAMYAR BEATZ" and the tagline
       came straight through the panel and crossed "Sound Kits", "About" and
       "Contact". A navigation menu has to be readable over whatever it
       happens to open on top of, so it gets a real background.
       The blur stays for the rounded edges and for anything translucent in
       the surface token, but it is no longer load-bearing. */
    background: var(--surface-1);
    backdrop-filter: blur(28px) saturate(180%);
    -webkit-backdrop-filter: blur(28px) saturate(180%);
    border: 1px solid var(--border-bright);
    box-shadow: var(--elev-3);
    max-height: calc(100dvh - var(--header-height) - var(--s-6));
    overflow-y: auto;
    z-index: 70;                       /* above the mini player (60) */
  }
  .main-nav.is-open { display: flex; animation: nav-in 260ms var(--ease-out); }
  @keyframes nav-in { from { opacity: 0; transform: translateY(-8px); } }

  /* NO full-page scrim here, deliberately. A `position: fixed` pseudo on
     .main-nav cannot be one: backdrop-filter makes this element a
     containing block for fixed descendants, and overflow-y clips them, so
     the "viewport" scrim came out 364x321 — the size of the panel — and sat
     invisibly behind its own opaque background. A scrim has to hang off an
     ancestor that has neither property. The panel being opaque is what the
     readability problem actually needed. */
  .main-nav a, .nav-logout button { width: 100%; padding: 13px var(--s-4); font-size: 15px; text-align: left; border-radius: var(--r-sm); }
  .main-nav a[aria-current="page"]::after { left: auto; right: var(--s-4); bottom: 50%; transform: translateY(50%); margin: 0; }
  .main-nav .nav-cta { text-align: center; justify-content: center; margin-top: var(--s-2); }
  .main-nav .nav-cart { justify-content: flex-start; }
}

@media (max-width: 700px) {
  .wrap { padding: 0 var(--s-4); }

  /* ------------------------------------------------------------------
     TABLES BECOME CARDS

     .table-scroll stops a wide table dragging the whole page sideways, and
     it does that job — no page here scrolls horizontally. But containing
     the overflow is not the same as making the content reachable: the cart
     needed 379px inside a 358px scroller, so "Remove" was cut to "Remov"
     with nothing to indicate the row could be scrolled, and the licence
     table needed 599px in the same space — five columns that no phone can
     show at once.

     A horizontal scrollbar inside a vertically-scrolling page is also a
     poor gesture on touch: the two axes fight, and the one you want is the
     one you did not get.

     So below 700px the rows stop being rows. Each becomes a card, and
     data-label (set in the templates) reinstates the column heading that
     <thead> was carrying. Nothing is clipped and nothing scrolls sideways.

     SCOPED TO THE TWO WIDE TABLES ON PURPOSE. Applied to every `table.data`
     it also caught the cart's totals table, which is a two-column
     label/value pair that already fitted: "Subtotal" became a card title
     with "$29.99" stranded on the line below it, so a summary that was one
     tidy line turned into two tall boxes. A table only wants this treatment
     when it genuinely cannot fit.
     ------------------------------------------------------------------ */
  table.data.cart-table, table.data.license-table,
  table.data.cart-table tbody, table.data.license-table tbody,
  table.data.cart-table tr, table.data.license-table tr,
  table.data.cart-table td, table.data.license-table td { display: block; width: 100%; }

  /* .table-scroll is left exactly as it is. `overflow-x: auto` only engages
     when there is something to scroll, and a stacked table fits — so the
     wrapper goes quiet on its own, and still does its job for the tables
     further down that are NOT stacked. */

  /* Headings move into each cell via data-label, so the real header row is
     redundant. Kept in the DOM rather than removed, so the table is still a
     table to a screen reader. */
  table.data.cart-table thead, table.data.license-table thead {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
  }

  table.data.cart-table tbody tr, table.data.license-table tbody tr {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    padding: var(--s-4);
    margin-bottom: var(--s-3);
  }
  /* No hover state on touch — it sticks after a tap. */
  table.data.cart-table tbody tr:hover, table.data.license-table tbody tr:hover { background: var(--surface-2); }
  table.data.cart-table tr:last-child, table.data.license-table tr:last-child { margin-bottom: 0; }

  table.data.cart-table td, table.data.license-table td {
    border-bottom: 0;
    padding: var(--s-2) 0;
    display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-4);
  }

  /* The label sits opposite its value, so a stacked row reads as a list of
     name/value pairs rather than as a column of orphaned strings. */
  table.data.cart-table td[data-label]::before, table.data.license-table td[data-label]::before {
    content: attr(data-label);
    font-family: var(--font-label); font-size: 10px; font-weight: 550;
    letter-spacing: .08em; text-transform: uppercase;
    color: var(--text-faint);
    flex: none;
  }

  /* The first cell is the row's title: full width, no label, heavier. */
  table.data.cart-table td:first-child, table.data.license-table td:first-child {
    display: block;
    font-size: 15px; font-weight: 600;
    padding: 0 0 var(--s-2);
  }
  table.data.cart-table td:last-child, table.data.license-table td:last-child { padding-right: 0; }

  /* Action cells go full width so the buttons are properly tappable. */
  .license-actions, .cart-remove {
    display: flex !important; gap: var(--s-2);
    padding-top: var(--s-3) !important;
  }
  /* Side by side and equal. Left to wrap, "Add to cart" sized to its text
     and "Buy now" filled the next line — two buttons of different widths on
     two rows, which reads as a hierarchy that isn't there. */
  .license-actions > *, .license-actions form { flex: 1 1 0; min-width: 0; }
  .license-actions form { display: flex; }
  .license-actions .btn { width: 100%; justify-content: center; }

  .hero { padding: var(--s-7) 0 var(--s-6); }
  .hero-video { min-height: 76vh; }
  .hero-copy { padding: var(--s-8) var(--s-4); }
  .hero-search { padding-left: var(--s-4); }
  /* Icon-only submit: at 390px the word "Search" costs a third of the pill,
     leaving no room to actually read what you typed. */
  .hero-search .btn span { display: none; }
  .hero-search .btn .icon { display: block; }
  .hero-search .btn { width: 44px; height: 44px; padding: 0; border-radius: 50%; }

  .grid { grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: var(--s-3); }
  .card-body { padding: var(--s-3); gap: var(--s-2); }
  .card-title { font-size: 14px; }

  /* Two cards across a 390px phone leaves ~148px of card interior. "from
     $29.99" and a "Licences" button need about 177px side by side, so the
     button was running past the card's right edge and getting clipped by
     its overflow:hidden — the price was legible and the thing you tap was
     not. Wrapping lets it drop to its own line and fill the width; on the
     cards where both do fit, nothing moves. */
  .card-actions { flex-wrap: wrap; gap: var(--s-2); }
  .card-actions .card-price { margin-right: 0; }
  .card-actions .btn { flex: 1 1 auto; justify-content: center; }

  /* Let the metadata take a second line rather than ellipsing. At this
     width "MELODIC TRAP · 128 BPM · G MAJOR" truncated to "128 BP…", which
     drops the key and the tempo — the two things a buyer scans for. */
  .beat-card .meta { white-space: normal; }

  /* Only the bottom half is tightened. --section-gap already scales the top
     margin down with the viewport, and overriding it here is what made the
     phone rhythm tighter than the desktop one at exactly the width where
     sections have no horizontal separation left to rely on. */
  .section-head { margin-bottom: var(--s-4); }

  .stat-cell { padding: var(--s-4); }
  .support-panel { padding: var(--s-5); }
  .support-panel .hero-actions { width: 100%; }
  .support-panel .btn { flex: 1; }
  .faq-list-lg summary { min-height: 60px; gap: var(--s-3); }
  .faq-list-lg .faq-answer { padding-right: 0; padding-left: calc(20px + var(--s-3)); }

  .totals, .checkout-form { max-width: none; }
  .license-actions { flex-direction: column; align-items: stretch; }
  .license-actions .btn { width: 100%; }
  .filter-actions { margin-left: 0; width: 100%; }
  .filter-actions .btn { flex: 1; }
  .filter-field.grow { flex-basis: 100%; }

  .mini-player { padding: 0 var(--s-2) var(--s-2); }
  .mini-player-inner {
    /* Flex, not the desktop grid. Two of the four zones are hidden below,
       and a 4-column grid with 2 hidden children reflows the survivors onto
       a second row instead of collapsing. Flex just closes the gap. */
    display: flex;
    gap: var(--s-2); padding: 0 var(--s-2); height: 60px;
  }
  .mp-identity { flex: 1; min-width: 0; }
  .mp-transport, .mp-actions { flex: none; }
  .mp-art { width: 38px; height: 38px; }
  /* Scoped to .mini-player deliberately: .wf-bars is shared with the beat
     cards, and hiding it unscoped stripped the waveform off every card. */
  .mini-player .mp-scrub,
  .mini-player .mp-volume,
  .mini-player .mp-share { display: none; }
  /* Favourite and close stay: saving a track one-handed is exactly the
     mobile use case, and a player with no way to dismiss it is a trap. */
  .mp-transport { gap: 0; }
  .mp-btn { width: 34px; height: 34px; }
  .mp-play { width: 40px; height: 40px; }

  .footer-top { flex-direction: column; gap: var(--s-5); }
  .footer-nav { flex-direction: row; flex-wrap: wrap; gap: var(--s-4); }
  .footer-bottom { flex-direction: column-reverse; align-items: flex-start; gap: var(--s-4); }

  .course-row { flex-direction: column; }
  .course-row-cover img { width: 100%; height: auto; aspect-ratio: 1/1; }
  .lesson-toolbar { flex-direction: column; align-items: stretch; }
  .lesson-nav .btn { flex: 1; }

  .about-inner { flex-direction: column; }
  .producer-photo { width: 100%; max-width: 260px; height: auto; aspect-ratio: 1/1; }
}

/* Backdrop-filter is the one thing here with no graceful degradation: with
   no support, `background: rgba(...)` alone leaves the header and player
   semi-transparent over scrolling content and text becomes unreadable.
   Opaque fallbacks. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .site-header, .mini-player-inner, .main-nav, .btn, .play-fab, .hero-search {
    background: var(--bg-elevated);
  }
  .btn-primary { background: var(--accent); }
  .hero-search { background: rgba(24,24,27,.92); }
}

/* The currency switcher's styles lived here. Both are gone: the shop
   quotes in the currency it settles in, so there is nothing to switch.
   See core/services/currency.py. */

/* The reference conversion beside a charged total. Small and dimmed on
   purpose: two figures at equal weight is a customer asking which one is
   real, which is the exact question this exists to prevent. */
.summary-alt {
  display: block; margin-top: 3px;
  font-size: 12px; color: var(--text-faint); font-weight: 400;
}

/* --- Payment method ------------------------------------------------------
   Rendered only when there is more than one, so this styles a real choice
   rather than a single decorative row. */
.paymethod { border: 0; padding: 0; margin: 0 0 var(--s-5); }
.paymethod-list { display: grid; gap: var(--s-2); margin-top: var(--s-3); }
.paymethod-option {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-4); border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--border);
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease-out),
              background-color var(--t-fast) var(--ease-out);
}
.paymethod-option:hover { border-color: var(--border-bright); }
/* :has, so the whole row reacts to its own radio rather than needing a
   class toggled by script. */
.paymethod-option:has(input:checked) {
  border-color: var(--accent); background: var(--accent-wash);
}
.paymethod-option input { flex: none; accent-color: var(--accent); }
.paymethod-body { display: flex; flex-direction: column; min-width: 0; }
.paymethod-label { font-size: 14px; font-weight: 550; }
/* What happens next. With one method this is most of the value of showing
   the block at all — the label alone says no more than the button would. */
.paymethod-note {
  margin-top: 3px; font-size: 12.5px; color: var(--text-dim); line-height: 1.5;
}

/* The only option: still stated, not asked. Reads as "this is how you will
   pay" rather than as a question with one answer. */
.paymethod-option.is-only {
  border-color: var(--border); background: var(--surface-2); cursor: default;
}
.paymethod-option.is-only:hover { border-color: var(--border); }
.paymethod-option.is-only input { pointer-events: none; }


/* ==========================================================================
   بخش الحاقی: نسخه‌ی پورتفولیو (SAMYAR BEATZ)
   هر چیزی که پایین این خط است به این پروژه اضافه شده و در فایل اصلی طراحی
   نبوده است. همه‌ی قوانین از توکن‌های :root بالای فایل استفاده می‌کنند،
   پس اگر رنگ یا فاصله‌ای را بالا عوض کنید، همه‌جا عوض می‌شود.
   ========================================================================== */

/* ---- دکمه‌ها: حرکت نرم و روشن‌شدن ----
   الهام از دکمه‌های One UI 8.5 سامسونگ: کمی بزرگ می‌شوند، کمی بالا می‌روند
   و روشن‌تر می‌شوند. اندازه‌ی بزرگ‌شدن ۳٪ است تا از جای خودشان بیرون نزنند.

   این حالت‌ها فقط وقتی اعمال می‌شوند که دستگاه hover واقعی دارد (ماوس).
   روی گوشی لمسی، hover پس از لمس می‌ماند و دکمه گیر می‌کند؛ برای همین
   آنجا حرکت حذف می‌شود.

   backdrop-filter از همه‌ی دکمه‌ها برداشته شده. روی گوشی‌های ضعیف، هر
   اسکرول یک بلور کامل از صفحه را دوباره حساب می‌کرد. حالا سطح ساده‌ی رنگی
   است. */
.btn {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transition: transform 260ms var(--ease-out),
              box-shadow 260ms var(--ease-out),
              background-color 260ms var(--ease-out),
              border-color 260ms var(--ease-out),
              color 260ms var(--ease-out);
}

@media (hover: hover) {
  .btn:hover {
    transform: translateY(-2px) scale(1.03);
  }
  .btn-primary:hover {
    background: var(--accent-bright);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .4), 0 10px 26px -10px rgba(250, 250, 250, .45);
  }
  .btn-ghost:hover {
    background: var(--accent-wash);
    border-color: var(--text-faint);
    color: var(--text);
    box-shadow: 0 10px 26px -14px rgba(244, 244, 245, .3);
  }
}

@media (hover: none) {
  .btn:hover { transform: none; }
}

.btn:active {
  transform: translateY(0) scale(.97);
  transition-duration: 120ms;
}

@media (prefers-reduced-motion: reduce) {
  .btn,
  .btn:hover,
  .btn:active,
  .donate-link:hover {
    transform: none !important;
  }
}

/* ---- بخش دونیت (صفحه‌ی About) ----
   دکمه‌ی NOWPayments دو فایل دارد: یکی برای تم تاریک (دکمه‌ی روشن روی زمینه‌ی
   تیره) و یکی برای تم روشن. با توجه به تم فعلی، یکی نشان داده می‌شود. */
.donate {
  display: grid;
  justify-items: center;
  gap: var(--s-5);
  width: 100%;
  min-width: 0;
}

.donate-link {
  display: inline-block;
  max-width: 100%;
  border-radius: var(--r-md);
  transition: transform 260ms var(--ease-out);
}

@media (hover: hover) {
  .donate-link:hover { transform: translateY(-2px) scale(1.02); }
}

.donate-img {
  display: block;
  width: min(100%, 360px);
  height: auto;
}

.donate-img-light { display: none; }
[data-theme="light"] .donate-img-dark { display: none; }
[data-theme="light"] .donate-img-light { display: block; }

/* کارت‌های کیف پول. min-width: 0 جلوی بیرون زدن آدرس‌های بلند از کارت را
   می‌گیرد، و overflow-wrap آدرس را می‌شکند بدون اینکه کارت را بزرگ کند. */
.wallet-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
  gap: var(--s-4);
  width: 100%;
  min-width: 0;
}

.wallet-card {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  min-width: 0;
  padding: var(--s-4);
  border-radius: var(--r-md);
  background: var(--surface-2);
  border: 1px solid var(--border);
}

.wallet-head {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-width: 0;
}

/* آیکون شبکه اختیاری است؛ اگر نباشد، فقط نام شبکه دیده می‌شود و جای خالی
   ایجاد نمی‌شود. */
.net-icon {
  width: 20px;
  height: 20px;
  flex: none;
  color: var(--text);
}

.wallet-net {
  min-width: 0;
  font-family: var(--font-label);
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  overflow-wrap: anywhere;
}

.wallet-address {
  font-family: var(--font-numeric);
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

.wallet-copy {
  align-self: flex-start;
  margin-top: auto;
}

.wallet-copy.is-copied {
  color: var(--text);
  background: var(--accent-wash);
  border-color: var(--border-bright);
}

/* ---- هدر در موبایل ----
   در عرض ۶۰۰ پیکسل یا کمتر، هدر دو ردیف می‌شود: ردیف اول لوگو و دکمه‌ی تم،
   ردیف دوم منو. به این ترتیب سه لینک (About، Licensing و Telegram) در هر
   عرضی جا می‌شوند، حتی ۳۲۰ پیکسل.

   هدر روی موبایل sticky نیست. یک هدر دو ردیفی که همیشه بالای صفحه بماند،
   یک چهارم صفحه‌ی گوشی را می‌گیرد؛ پس با اسکرول کنار می‌رود. */
@media (max-width: 600px) {
  .site-header { position: static; }
  .header-inner {
    height: auto;
    flex-wrap: wrap;
    row-gap: var(--s-2);
    padding-block: var(--s-3);
  }
  .header-actions { margin-left: auto; }
  .main-nav { order: 3; flex: 1 0 100%; margin-left: 0; }
  .main-nav a { padding: 8px 10px; }
  .wordmark .brand-logo { height: 24px; }
}

/* ---- پنل ادمین ----
   صفحه‌ی ادمین کارت ساده‌ای است؛ منوی عمومی و فوتر ندارد. فیلدها از قانون‌های
   عمومی input در همین فایل می‌آیند. */
.admin-card {
  max-width: 440px;
  margin: var(--s-8) auto;
  padding: var(--s-6);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  border: 1px solid var(--border);
}
.admin-card h1 { margin: 0 0 var(--s-4); }
.admin-form { display: grid; gap: var(--s-4); margin-top: var(--s-5); }
.field { display: grid; gap: var(--s-2); }
.field > span { font-size: 13px; color: var(--text-dim); }
.form-error {
  margin: 0 0 var(--s-4);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  border: 1px solid var(--danger);
  color: var(--text);
  font-size: 14px;
  line-height: 1.5;
}
.admin-actions { margin-top: var(--s-5); }
.admin-note { margin: var(--s-5) 0 0; font-size: 13px; color: var(--text-faint); }

/* ---- پنل ادمین: ویرایشگر ----
   چیدمان ساده و بی‌تزیین. هر بخش یک کادر است، هر ردیف یک اسلات با فاصله‌ی
   یکسان، و دکمه‌های ذخیره همیشه در انتهای همان بخش هستند. */
.admin-wrap { max-width: 880px; margin: var(--s-6) auto; }
.admin-top {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--s-4); margin-bottom: var(--s-4);
}
.admin-top .eyebrow { margin: 0; }
.admin-signout { margin: 0; }

.admin-tabs {
  display: flex; flex-wrap: wrap; gap: var(--s-1);
  border-bottom: 1px solid var(--border); margin-bottom: var(--s-5);
}
.admin-tabs a {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: 0 var(--s-4); color: var(--text-dim); text-decoration: none;
  border-radius: var(--r-md) var(--r-md) 0 0;
}
.admin-tabs a:hover { color: var(--text); }
.admin-tabs a[aria-current="page"] { color: var(--text); box-shadow: inset 0 -2px 0 var(--accent); }

.admin-flash {
  margin: 0 0 var(--s-5); padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md); border: 1px solid var(--border); font-size: 14px; line-height: 1.5;
}
.admin-flash.is-ok { border-color: var(--success); }
.admin-flash.is-error { border-color: var(--danger); }

.admin-group {
  min-width: 0; margin: 0 0 var(--s-5); padding: var(--s-5);
  border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--surface-1);
}
.admin-group legend { padding: 0 var(--s-2); font-weight: 600; font-size: 15px; }
.admin-group h2 { margin: 0 0 var(--s-4); font-size: 15px; }
.admin-hint { margin: 0 0 var(--s-4); font-size: 13px; color: var(--text-dim); line-height: 1.5; }

.admin-grid {
  display: grid; gap: var(--s-4); margin-bottom: var(--s-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
}
.admin-cells {
  display: grid; gap: var(--s-3); margin-top: var(--s-3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
}
.admin-slot { padding: var(--s-4) 0; border-top: 1px dashed var(--border); min-width: 0; }
.admin-slot:first-of-type { border-top: 0; padding-top: 0; }
.admin-slot-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-3); margin-bottom: var(--s-3);
}
.admin-slot-head .admin-slot-title { margin: 0; }
.admin-slot-title {
  display: block; margin-bottom: var(--s-2);
  font-size: 12px; letter-spacing: .04em; color: var(--text-faint);
}
.admin-radio {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: 44px; font-size: 13px; color: var(--text-dim);
}

.field { min-width: 0; }
.field-hint { font-size: 12px; color: var(--text-faint); line-height: 1.4; }

.admin-submit {
  display: flex; justify-content: flex-end; margin-top: var(--s-5);
}

.admin-photo-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-5); }
.admin-photo-preview {
  width: 96px; height: 96px; flex: none; border-radius: 50%; object-fit: cover;
  border: 1px solid var(--border);
}
.admin-photo-preview.is-empty {
  display: grid; place-items: center; background: var(--surface-3);
  font-family: var(--font-display); font-size: 36px; color: var(--text-dim);
}
.admin-photo-actions { display: grid; gap: var(--s-3); flex: 1 1 240px; min-width: 0; }
.admin-photo-form { display: grid; gap: var(--s-3); }
.admin-photo-form input[type="file"] { min-height: 44px; font-size: 14px; }
.admin-photo-remove { margin: 0; }

/* ==========================================================================
   اصلاح چیدمان: کارت‌های وسط‌چین، آیکون‌های شبکه‌ها، و منوی موبایل
   ========================================================================== */

/* کارت‌ها: حداکثر سه ستون در هر ردیف و همه وسط‌چین. ردیف آخر هم وسط می‌ماند:
   ۵ کارت = ۳ + ۲، و ۶ کارت = ۳ + ۳. در موبایل هر کارت یک ردیف است تا متن
   آدرس کیف پول خوانا بماند. */
.tree-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-4);
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}
.tree-grid > * {
  flex: 0 0 100%;
  max-width: 100%;
  min-width: 0;
}
@media (min-width: 640px) {
  .tree-grid > * {
    flex-basis: calc((100% - 2 * var(--s-4)) / 3);
    max-width: calc((100% - 2 * var(--s-4)) / 3);
  }
}

/* فاصله‌ی دو طرف صفحه: کارت‌ها و متن هرگز به لبه‌ی گوشی نمی‌چسبند. */
.wrap { padding-inline: var(--s-5); }
@media (max-width: 600px) {
  .wrap { padding-inline: var(--s-4); }
}

/* آیکون‌های شبکه‌های اجتماعی. آیکون‌های برندی پرشده‌اند، پس fill باید رنگ
   متن باشد. قاعده‌ی .icon (fill: none) برای آیکون‌های خطی است و اینجا
   بازنویسی می‌شود. اندازه‌ی لمس حداقل ۴۴ پیکسل است. */
.social-link { width: 44px; height: 44px; }
.social-link .icon { width: 20px; height: 20px; fill: currentColor; }
.social-rail-lg .social-link { width: 52px; height: 52px; }
.social-rail-lg .social-link .icon { width: 22px; height: 22px; }

/* منو در موبایل: همیشه دیده می‌شود و نیازی به دکمه‌ی همبرگری (که ساخته
   نشده) ندارد. لینک‌ها حداقل ۴۴ پیکسل ارتفاع لمس دارند. */
@media (max-width: 820px) {
  .nav-toggle { display: none; }
  .main-nav {
    display: flex;
    position: static;
    inset: auto;
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--s-1);
    padding: 0;
    margin: 0;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .main-nav a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    padding: 0 var(--s-3);
  }
}

/* دکمه‌ها روی لمس حداقل ۴۴ پیکسل ارتفاع دارند (قانون لمس). */
.btn { min-height: 44px; }
.footer-nav a { min-height: 44px; display: inline-flex; align-items: center; }
@media (max-width: 600px) {
  .masthead-title { font-size: clamp(2.25rem, 10.5vw, 3.5rem); }
}
@media (max-width: 820px) {
  .main-nav a[aria-current="page"]::after { display: none; }
  .main-nav a[aria-current="page"] { text-decoration: underline; text-underline-offset: 6px; }
}
