
:root {
    --ink: #0a090d;
    --violet: #8a2be2;
    --violet-deep: #9400d3;
    --silver: #e6e6fa;
    --soulskin: #c71585;
    --speed: 0.3s ease-in-out;
    --surface: rgba(255,255,255,.05);
    --surface-strong: rgba(10,9,13,.9);
    --bar: rgba(5,4,7,.94);
    --header-bg: rgba(10,9,13,0.75);
    /* one source of truth for the sticky header's height: the header sets
       its own min-height from this, and the full-bleed mobile modal starts
       below it so the brand mark is never buried under a dialog */
    --site-header-h: 84px;
    --shade-1: rgba(10,9,13,.55);
    --shade-2: rgba(10,9,13,.92);
    --input-bg: rgba(0,0,0,.35);
    /* A plain, functional system-ui stack - no downloaded webfonts, so it
       reads as native on every platform. Headings use it too, not a
       locked-in display face like the old Cinzel/Space-Mono split. */
    --font-active: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; scrollbar-color: var(--violet) var(--ink); }
body {
    display: flex;
    flex-direction: column;
    /* 1px short of the full viewport: on some displays 100vh rounds a
       fraction of a device pixel taller than the actual layout viewport,
       which is enough to trigger a permanent vertical scrollbar on pages
       that otherwise fit exactly. The 1px is imperceptible; genuinely
       tall pages already exceed 100vh by far more than that. */
    min-height: calc(100vh - 1px);
    font-family: var(--font-active);
    background-color: var(--ink);
    color: var(--silver);
    background-image: radial-gradient(ellipse 80% 50% at 70% -10%, rgba(138,43,226,.08), transparent 60%);
}
h1, h2, h3 { font-family: inherit; font-weight: 700; }
h1 { font-size: clamp(2rem, 5vw, 4rem); line-height: 1.1; }
h2 { font-size: 2.2rem; margin-bottom: 1.5rem; }
a { text-decoration: none; color: var(--violet); transition: color var(--speed); }
a:hover { color: var(--violet-deep); }
[hidden] { display: none !important; }
.muted { color: var(--silver); opacity: .55; font-size: .86rem; }
.btn {
    display: inline-block;
    padding: 0.8rem 2rem;
    border: 2px solid var(--violet);
    background: transparent; color: var(--silver);
    text-transform: uppercase; font-weight: 700; font-family: inherit; letter-spacing: 1px;
    cursor: pointer; transition: all var(--speed);
}
.btn:hover { background: var(--violet); color: var(--ink); transform: translateY(-2px); }

/* base form controls - same values as theme.css's copy (dashboard/login),
   duplicated here because sitePage() (/, /login, /backstage, /archive)
   never loads /theme.css, only this stylesheet */
input:not([type]), input[type=text], input[type=search], input[type=email],
input[type=password], input[type=number], input[type=url], input[type=date],
select, textarea {
    background: var(--input-bg);
    color: var(--silver);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: .8em 1em;
    font-family: inherit;
    font-size: 1rem;
    transition: border-color var(--speed), box-shadow var(--speed);
}
input:not([type]):focus, input[type=text]:focus, input[type=search]:focus,
input[type=email]:focus, input[type=password]:focus, input[type=number]:focus,
input[type=url]:focus, input[type=date]:focus,
select:focus, textarea:focus {
    outline: none;
    border-color: var(--violet);
    box-shadow: 0 0 0 3px var(--violet-pale);
}
select option { background: var(--bar); color: var(--silver); }
/* input[type=date] draws a native calendar button and popup that follow the
   page's declared color-scheme, not our background/color - without this the
   picker glyph is near-invisible on the dark field it sits in. */
input[type=date] { color-scheme: dark; }


.container { width: 92%; max-width: 1400px; margin: 0 auto; padding: 2rem 0; }

/* background bed */
#bg-video { position: fixed; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -1; }
.bg-shade { position: fixed; inset: 0; z-index: -1; background: linear-gradient(180deg, var(--shade-1), var(--shade-2)); pointer-events: none; }

/* header / nav.
   SCOPED TO body > header ON PURPOSE, and it must stay that way. As a bare
   element selector this handed every other <header> on the page - the
   account hub's three .view-head section headers, the modal's title bar -
   an opaque var(--ink) background AND position: sticky; top: 0; z-index:
   500. Same z-index as the real site header but later in the document, so
   the content's own black box scrolled up and painted straight over the
   brand mark. The header is for header things only; everything else rolls
   under it. */
body > header {
    position: sticky; top: 0; z-index: 500;
    min-height: var(--site-header-h); display: flex; align-items: center;
    border-bottom: 1px solid rgba(138,43,226,.25);
    background: var(--ink);
}
.nav-content { width: 100%; position: relative; display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; padding: 20px 40px; }
.logo a { font-size: 1.6rem; font-weight: 900; color: var(--silver); letter-spacing: 3px; display: block; }
.logo img { display: block; height: 60px; width: auto; }
.nav-links ul { list-style: none; display: flex; gap: 1.6rem; flex-wrap: wrap; }
.nav-links a { font-size: .95rem; text-transform: uppercase; padding: .5rem 0; border-bottom: 2px solid transparent; }
.nav-links a:hover { color: var(--silver); border-bottom-color: var(--violet); }
header.nav-align-center .nav-content { position: relative; }
header.nav-align-center .nav-links { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }

/* mobile drawer - zero JS, checkbox + label + ~ sibling combinator, same
   convention as the <details> account dropdown below */
.nav-burger-toggle { position: absolute; opacity: 0; pointer-events: none; }
.hamburger {
    display: none;
    flex-direction: column; justify-content: center; gap: 5px;
    width: 32px; height: 32px; padding: 6px;
    cursor: pointer; flex-shrink: 0;
}
.hamburger span { display: block; width: 100%; height: 2px; background: var(--silver); border-radius: 0; transition: transform .25s ease, opacity .2s ease; }
.nav-burger-toggle:checked ~ .hamburger span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-burger-toggle:checked ~ .hamburger span:nth-child(2) { opacity: 0; }
.nav-burger-toggle:checked ~ .hamburger span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* account nav slot - icon buttons when signed out; a zero-JS <details>
   dropdown once someone is signed in. Same square, sharp-cornered icon
   language as the dashboard's own nav, so the two don't read as different
   products. */
.nav-links li.backstage { list-style: none; display: flex; align-items: center; }
.bs-nav-stranger { gap: .5rem; }
.bs-nav-icon {
    display: flex; align-items: center; justify-content: center;
    width: 2.4rem; height: 2.4rem;
    border: 1px solid rgba(138,43,226,.4); background: rgba(138,43,226,.12);
    color: var(--silver);
    transition: border-color .2s, color .2s;
}
.bs-nav-icon:hover { border-color: var(--violet); color: var(--violet); }
.bs-nav-menu { position: relative; }
.bs-nav-menu summary { list-style: none; cursor: pointer; display: block; }
.bs-nav-menu summary::-webkit-details-marker { display: none; }
.bs-nav-avatar, .bs-nav-initial {
    display: flex; align-items: center; justify-content: center;
    width: 2.4rem; height: 2.4rem;
    border: 1px solid rgba(138,43,226,.4); background: rgba(138,43,226,.12);
    object-fit: cover; font-weight: 700; font-size: .86rem;
    transition: border-color .2s, box-shadow .2s;
}
.bs-nav-menu[open] summary .bs-nav-avatar, .bs-nav-menu[open] summary .bs-nav-initial,
.bs-nav-menu summary:hover .bs-nav-avatar, .bs-nav-menu summary:hover .bs-nav-initial {
    border-color: var(--violet);
}
.bs-nav-dropdown {
    position: absolute; top: 100%; right: 0; z-index: 500;
    display: flex; flex-direction: column; min-width: 11rem;
    background: var(--ink);
    border: 1px solid rgba(138, 43, 226, .28); border-top: 2px solid var(--violet);
    box-shadow: 0 8px 20px rgba(0, 0, 0, .4);
    overflow: hidden;
}
.bs-nav-dropdown a, .bs-nav-dropdown button {
    padding: .7em 1em; font-size: .86rem; text-transform: none; letter-spacing: normal;
    border-bottom: 1px solid rgba(138,43,226,.15);
}
.bs-nav-dropdown a:last-child, .bs-nav-dropdown button:last-child { border-bottom: none; }
.bs-nav-dropdown a:hover, .bs-nav-dropdown button:hover { background: rgba(138,43,226,.12); }
.bs-nav-signout { display: contents; }
.bs-nav-signout button {
    width: 100%; text-align: left; background: none; border: none; border-radius: 0;
    color: var(--silver); font-family: inherit; cursor: pointer;
}

/* main */
main { flex: 1; min-height: 0; width: 100%; }
.ve-sections { display: grid; grid-template-columns: repeat(12, 1fr); gap: 0 2.5rem; }
.ve-section { padding: 40px 0 10px; }
.ve-section[data-span="span 12"] { grid-column: span 12; }
.ve-section[data-span="span 8"] { grid-column: span 8; }
.ve-section[data-span="span 6"] { grid-column: span 6; }
.ve-section[data-span="span 4"] { grid-column: span 4; }
.hero-kicker { color: var(--soulskin); font-size: 1rem; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 1.2rem; }
.hero-split { display: flex; gap: 3rem; align-items: stretch; position: relative; z-index: 2; }
.hero-headline { margin-bottom: 1.5rem; }
.hero-subtitle { font-size: clamp(1rem, 1.6vw, 1.3rem); margin-bottom: 2rem; color: var(--soulskin); max-width: 46ch; }
.hero-shot { flex: 0 0 auto; width: auto; max-width: min(20vw, 380px); height: auto; max-height: calc(100vh - 240px); align-self: flex-start; display: block; border: 1px solid rgba(138,43,226,.25); }
/* the whole card is the link */
a.product-card { color: inherit; display: flex; }
a.product-card:hover { color: inherit; }
.hero-shot-spacer { flex: 0 0 auto; width: min(20vw, 380px); }

/* merch */
.product-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 28px; margin-top: 30px; }
.product-card { background: var(--surface); border: 1px solid rgba(138,43,226,.25); padding: 18px; display: flex; flex-direction: column; transition: transform .3s ease, box-shadow .3s ease; }
.product-card:hover { transform: translateY(-4px); box-shadow: 0 8px 16px rgba(0,0,0,.3); }
.product-image-wrapper { height: 260px; overflow: hidden; margin-bottom: 14px; position: relative; border: 1px solid rgba(138,43,226,.3); }
.product-card img { width: 100%; height: 100%; object-fit: cover; transition: transform .5s ease; }
.product-card:hover img { transform: scale(1.08); }
.product-title { font-size: 1.25rem; margin-bottom: 5px; color: var(--silver); }
.product-type { font-size: .86rem; text-transform: uppercase; color: var(--soulskin); display: block; margin-bottom: 8px; letter-spacing: 1px; }
.product-desc { font-size: .86rem; margin-bottom: 8px; opacity: .75; }
.price { font-size: 1.5rem; font-weight: 700; margin-top: auto; color: var(--violet); padding-top: 12px; }
.product-card .btn { margin-top: 14px; padding: .55rem 1.2rem; font-size: .86rem; }

/* footer */
footer { min-height: 28px; display: flex; align-items: center; background: var(--ink); border-top: 1px solid rgba(138,43,226,.25); }
.footer-content { width: 100%; max-width: 1400px; margin: 0 auto; padding: 20px 4%; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; font-size: .86rem; letter-spacing: 1px; text-transform: uppercase; }
.footer-left { text-align: left; opacity: .75; }
.footer-center { display: flex; gap: .9rem; align-items: center; }
.footer-center a { color: var(--silver); opacity: .8; }
.footer-center a:hover { color: var(--violet); opacity: 1; }
.footer-right { text-align: right; opacity: .75; }

.kind-icon { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: var(--silver); opacity: .4; }
.kind-icon svg { width: 30%; height: 30%; max-width: 3.4rem; max-height: 3.4rem; }

@media (max-width: 1024px) {
    .hero-split { flex-direction: column; }
    .hero-shot { flex: none; width: 100%; max-width: 420px; }
    .ve-sections { grid-template-columns: 1fr !important; }
    .ve-section { grid-column: 1 !important; }
}
@media (max-width: 760px) {
    .hamburger { display: flex; }
    /* same halving as dashboard/main.css's own copy of this rule - kept in
       sync per the "identical box model" note on .nav-content/.logo above */
    .logo img { height: 30px; }
    .nav-content { padding: 16px 20px; }
    header.nav-align-center .nav-links, .nav-links {
        display: none;
    }
    .nav-burger-toggle:checked ~ .nav-links {
        display: block;
        position: absolute; top: 100%; left: 0; right: 0; transform: none;
        z-index: 600;
        /* fully opaque, not a translucent/blurred tint: this drawer sits
           directly over the hero text, and any see-through reads as
           bleed-through rather than depth */
        background: var(--ink);
        border-top: 1px solid rgba(138,43,226,.2);
        border-bottom: 2px solid rgba(138,43,226,.3);
    }
    .nav-burger-toggle:checked ~ .nav-links ul {
        flex-direction: column; align-items: flex-start; gap: 0; width: 100%;
    }
    .nav-burger-toggle:checked ~ .nav-links ul li { width: 100%; }
    .nav-burger-toggle:checked ~ .nav-links ul li a {
        display: block; width: 100%; padding: .9rem 1.4rem;
        border-bottom: 1px solid rgba(138,43,226,.1);
    }
    .nav-burger-toggle:checked ~ .nav-links ul li.backstage { padding: .7rem 1.4rem; flex-wrap: wrap; }
    /* the account dropdown is normally position:absolute, anchored right:0
       against its own small avatar box - fine on desktop, where that box
       sits at the far right of a narrow header. Inside the mobile drawer
       the avatar box is left-aligned in a full-width row instead, so
       right:0 shot the whole panel off past the left edge of the screen,
       leaving the drawer's own opaque background behind and exposing
       whatever page content sits underneath. Make it a normal block in
       flow instead - it can never escape its container's width that way,
       and it reads as a natural sub-list under the avatar row, the same
       shape as every other drawer row. */
    .bs-nav-menu { width: 100%; }
    .bs-nav-dropdown {
        position: static; width: 100%; min-width: 0;
        margin-top: .5rem; border-left: none; border-right: none; box-shadow: none;
    }
}
@media (max-width: 640px) {
    .nav-links ul { gap: .9rem; }
    .footer-content { grid-template-columns: 1fr; text-align: center; gap: .4rem; }
    .footer-left, .footer-center, .footer-right { justify-content: center; text-align: center; }
}
:root {
    --border: rgba(138, 43, 226, .28);
    --violet-pale: rgba(138, 43, 226, .14);
    --ink-soft: rgba(230, 230, 250, .62);
    --danger: #FF6B6B;
    --danger-pale: rgba(255, 107, 107, .14);
    --sec: 3.2rem;
}
.error { color: var(--danger); }

/* login/join: a floating panel within the page, not a full-viewport
   takeover - the real header, footer and background bed stay visible
   around it, the same as everywhere else on the site */
.login-stage {
    display: flex; align-items: center; justify-content: center;
    min-height: calc(100vh - 84px - 4rem); padding: 2rem 1rem;
}
.login-panel {
    position: relative; width: 24rem; max-width: 100%;
    display: flex; flex-direction: column; gap: 1.05rem; text-align: center;
    padding: 3rem 2.6rem 2.8rem;
    background: var(--ink);
    border: 1px solid rgba(138, 43, 226, .45); border-top: 3px solid var(--violet);
    border-radius: 0;
}
.login-logo { display: block; height: 4.2rem; width: auto; margin: 0 auto; }
.login-sub { color: var(--ink-soft); font-size: .86rem; text-transform: uppercase; letter-spacing: 3px; margin: 0 0 .4rem; }
.login-rule { border: none; border-top: 1px solid rgba(138, 43, 226, .3); margin: .2rem 0; }
.join-perks {
    list-style: none; padding: 0; margin: 0 0 .4rem;
    display: flex; flex-direction: column; gap: .5rem;
    text-align: left; font-size: .9rem; color: var(--silver);
}
.join-perks li { padding-left: 1.6rem; position: relative; line-height: 1.4; }
.join-perks li::before {
    content: ''; position: absolute; left: 0; top: .28em;
    width: .9rem; height: .9rem;
    border: 1px solid var(--violet); border-radius: 0;
    background: var(--violet-pale);
}
.join-perks li::after {
    content: ''; position: absolute; left: .22rem; top: .5em;
    width: .42rem; height: .22rem;
    border-left: 2px solid var(--violet); border-bottom: 2px solid var(--violet);
    transform: rotate(-45deg);
}
.login-panel input {
    width: 100%; text-align: center; padding: .9em 1em; border-radius: 0; letter-spacing: .5px;
    background: rgba(0, 0, 0, .38); border-color: rgba(138, 43, 226, .42); color: var(--silver);
}
.login-panel input:focus { border-color: var(--violet); box-shadow: 0 0 0 3px rgba(138, 43, 226, .35); }
.login-panel .btn { padding: .95rem; letter-spacing: 2px; font-size: .86rem; }
.login-panel .error { font-size: .86rem; margin: 0; }

/* account/purchases/item cards */
.card {
    background: var(--surface-strong);
    border: 1px solid var(--border); border-top: 2px solid var(--violet); border-radius: 0;
    padding: 1.5rem 1.6rem; margin-bottom: var(--sec);
    display: flex; flex-direction: column; gap: .9rem; align-items: flex-start;
}
.card label { display: flex; flex-direction: column; gap: .3rem; width: 100%; font-size: .86rem; color: var(--ink-soft); text-transform: uppercase; letter-spacing: 1px; }
.card input { background: var(--input-bg); color: var(--silver); }
.card h2 { margin-top: .1rem; }
.card .form-actions, .card > a.btn { margin-left: auto; }
.form-actions { display: flex; gap: .55rem; justify-content: flex-end; margin-top: .5rem; }

.avatar-wrap { position: relative; flex: 0 0 auto; width: 3.4rem; height: 3.4rem; border-radius: 0; overflow: hidden; border: 1px solid var(--border); background: var(--violet-pale); }
.avatar-img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 0; }
.avatar-fallback { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-weight: 700; color: var(--silver); font-size: 1rem; }
.avatar-fallback.lg { font-size: 1.35rem; }

.menu-rule { border: none; border-top: 1px solid rgba(138, 43, 226, .2); margin: .55rem 0; }
.menu-item {
    display: flex; align-items: center; justify-content: space-between; gap: .5rem; width: 100%;
    border: none; background: transparent; color: var(--silver);
    padding: .55em .5em; border-radius: 0; cursor: pointer; font-size: .86rem; text-align: left;
    text-decoration: none; transition: background var(--speed), color var(--speed);
}
.menu-item:hover { background: var(--violet-pale); color: #fff; }
.menu-item.danger { color: var(--danger); }
.menu-item.danger:hover { background: var(--danger-pale); color: var(--danger); }

.bs-wrap { max-width: 30rem; margin: 0 auto; padding: 3rem 20px 5rem; }
.bs-wrap.wide { max-width: 40rem; }
/* the account page: same rail-plus-work shell as the dashboard, so it needs
   the dashboard's own width rather than the narrow reading column every
   other backstage page (login, item, thanks) uses */
.bs-wrap.shell { max-width: 78rem; padding: 2.4rem 20px 5rem; }
/* Username [PFP] on one row, an email/verified subtext line under it, then
   the nav - the account page's own masthead, distinct from the outer site
   header above it. avatar-first in source order would put focus there on
   tab through a screen reader before the name that identifies the page;
   order:2 keeps it visually on the right without reordering the DOM. */
.bs-hub { display: flex; align-items: center; justify-content: space-between; gap: 1rem; width: 100%; flex-wrap: wrap; }
.bs-hub-id { display: flex; flex-direction: column; gap: .2rem; min-width: 0; }
.bs-hub-id strong { color: var(--silver); font-size: 1.3rem; font-weight: 700; }
.bs-hub-sub { color: var(--ink-soft); font-size: .86rem; margin: .4rem 0 1.6rem; }
.bs-avatar-form { order: 2; }
.bs-avatar-remove { order: 3; flex: 0 0 100%; display: flex; justify-content: flex-end; margin-top: -.6rem; }
.bs-menu { display: flex; flex-direction: column; gap: .2rem; width: 100%; }
.bs-back { display: inline-block; margin-bottom: 1.4rem; font-size: .86rem; text-transform: uppercase; letter-spacing: 2px; }

/* the account page's own rail-plus-work shell, byte-for-byte the dashboard's
   .layout/.rail/.rail-item/.work/.view-head values (dashboard/main.css) -
   same class names, same numbers, so this reads as the same workspace
   rather than a lookalike of it. Anchors, not buttons: there is no script
   here to drive a JS view switcher, so "which page" is just which anchor
   the browser jumped to. */
.layout { display: flex; align-items: stretch; gap: 0; }
.rail {
    width: 13.5rem; flex: 0 0 13.5rem;
    display: flex; flex-direction: column; gap: 1.3rem;
    padding: 1.4rem 1rem;
    background: linear-gradient(180deg, rgba(148, 0, 211, .22), rgba(10, 9, 13, .92) 42%), var(--bar);
    border-right: 1px solid rgba(138, 43, 226, .25);
    color: var(--silver);
}
.rail-items { display: flex; flex-direction: column; gap: .3rem; }
.rail-item {
    display: block; text-align: left;
    border: 1px solid transparent;
    background: transparent;
    color: var(--ink-soft);
    padding: .6em .95em;
    border-radius: 0;
    cursor: pointer;
    font-weight: 700;
    font-size: .86rem;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    transition: all var(--speed);
}
.rail-item:hover { color: var(--silver); background: var(--violet-pale); border-color: rgba(138, 43, 226, .2); }
/* Matches dashboard/main.css's own .work padding (CLAUDE.md: same class
   names, same values) minus its +100px right-side reserve for dashboard-
   only floating chrome this shell doesn't have. Was 0 0 0 2.2rem - no top,
   right or bottom padding at all, which is why the search box and the deck
   butted straight into each other and the rail. */
.work { flex: 1; min-width: 0; padding: 1.6rem 2.2rem 4rem 2.2rem; }
.view-head { margin-bottom: var(--sec); }
.view-head h1 {
    font-size: clamp(1.9rem, 3.4vw, 2.9rem);
    letter-spacing: 4px;
    color: var(--silver);
    text-shadow: 0 0 30px rgba(138, 43, 226, .55), 0 4px 14px rgba(138, 43, 226, .25);
    margin: 0 0 .5rem;
}
.view-head .muted { font-size: .86rem; letter-spacing: .3px; }
.view-section + .view-section { margin-top: 4rem; }
@media (max-width: 860px) {
    .layout { flex-direction: column; }
    .rail { width: auto; flex: none; flex-direction: row; flex-wrap: wrap; border-right: none; border-bottom: 1px solid rgba(138, 43, 226, .25); }
    .rail-items { flex-direction: row; flex-wrap: wrap; }
    .work { padding: 1.6rem 0 0; }
}

/* status pills - same values as theme.css's .badge (dashboard), duplicated
   here because sitePage() never loads /theme.css */
.badge {
    display: inline-block; font-size: .86rem; font-weight: 700;
    padding: .12em .6em; border-radius: 999px; border: 1px solid var(--border);
    color: var(--silver); opacity: .85; text-transform: uppercase; letter-spacing: .5px;
}
.badge.on { background: var(--violet); border-color: var(--violet); color: #fff; opacity: 1; }
.badge.dirty { background: var(--soulskin); border-color: var(--soulskin); color: var(--ink); opacity: 1; }

/* purchases table - same shape as the dashboard's .prod-table (products
   list): bordered header row, hover state, right-aligned actions column.
   No backdrop-filter blur here - unlike the dashboard's scrolling panels,
   this page has nothing moving behind the table for it to blur. */
.bs-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: .9rem; background: var(--surface-strong); border: 1px solid var(--border); border-top: 2px solid var(--violet); border-radius: 0; overflow: hidden; }
.bs-table th, .bs-table td { text-align: left; padding: .95rem 1.1rem; border-bottom: 1px solid rgba(138, 43, 226, .12); vertical-align: middle; }
.bs-table thead th {
    color: var(--ink-soft); font-size: .86rem; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 700;
    background: rgba(138, 43, 226, .08); border-bottom: 1px solid rgba(138, 43, 226, .3);
    padding-top: .8rem; padding-bottom: .8rem;
}
.bs-table tbody tr { transition: background var(--speed); }
.bs-table tbody tr:last-child td { border-bottom: none; }
.bs-table tbody tr:hover td { background: var(--violet-pale); }
.bs-table td strong { display: block; color: var(--silver); font-size: 1rem; font-weight: 700; }
.bs-table .tk { display: block; font-size: .86rem; color: var(--ink-soft); }
.bs-table .acts { text-align: right; white-space: nowrap; }
/* landing on a card via an anchor jump must not tuck it under the sticky
   site header */
[id] { scroll-margin-top: 6rem; }

/* the avatar itself is the "change photo" control: a <label> wrapping a
   hidden file input, with a small pencil badge overlaid - no click-to-open
   JS needed, a <label> already does that natively */
.bs-avatar-form { display: flex; align-items: center; gap: .8rem; }
/* The account masthead avatar is a page-level identity element, not a menu
   row: double the 3.4rem base so a face is actually recognisable, and so
   the (unchanged, 1.6rem) edit badge sits in a corner of it rather than
   over the middle. This is the ONE place that departs from the shared
   avatar sizes - the dashboard's own menu avatars are untouched. */
.bs-avatar-picker { cursor: pointer; width: 6.8rem; height: 6.8rem; }
.bs-avatar-picker .avatar-fallback { font-size: 2.2rem; }
.avatar-edit-btn {
    position: absolute; right: -2px; bottom: -2px;
    width: 1.6rem; height: 1.6rem;
    display: flex; align-items: center; justify-content: center;
    border: 1px solid var(--border);
    background: var(--ink); color: var(--silver);
    cursor: pointer; padding: 0;
    transition: border-color var(--speed), color var(--speed);
}
.avatar-edit-btn:hover { border-color: var(--violet); color: var(--violet); }
.avatar-edit-btn svg { width: 12px; height: 12px; }
.bs-avatar-remove { margin-left: -.4rem; }
.visually-hidden {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.bs-form-wide { width: 100%; }
.bs-title { font-size: 1.4rem; font-weight: 700; margin: 0 0 1.2rem; color: var(--silver); }

.bs-filter-row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: 1rem; }
.bs-lore-cats { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
.bs-lore-progress { margin-bottom: .8rem; }
/* A native <progress> - CSP (style-src 'self', no 'unsafe-inline') drops an
   inline width style outright, which is what the div+span this replaced
   silently relied on. appearance:none strips the OS chrome; the track and
   fill are then styled through the two vendor pseudo-elements a <progress>
   actually exposes (Firefox has no equivalent to ::-webkit-progress-bar -
   its unstyled default background already matches --border closely enough
   that no separate track rule is needed there). */
.bs-lore-progress-bar {
    appearance: none; -webkit-appearance: none; -moz-appearance: none;
    display: block; width: 100%; height: .4rem; border: none; border-radius: 0;
    background: var(--border);
}
.bs-lore-progress-bar::-webkit-progress-bar { background: var(--border); border-radius: 0; }
.bs-lore-progress-bar::-webkit-progress-value { background: var(--violet); border-radius: 0; }
.bs-lore-progress-bar::-moz-progress-bar { background: var(--violet); border-radius: 0; }
.bs-lore-progress p { margin-top: .35rem; }
.bs-pager { display: flex; align-items: center; gap: .8rem; margin-top: 1rem; }
.bs-pager-pos { font-size: .86rem; color: var(--ink-soft); }
.bs-item-meta { font-size: .86rem; color: var(--ink-soft); }
.bs-price { font-size: 1.05rem; font-weight: 700; color: var(--silver); white-space: nowrap; }
.bs-empty { color: var(--ink-soft); font-size: .86rem; line-height: 1.6; }
.bs-note { font-size: .86rem; color: var(--ink-soft); line-height: 1.6; margin-top: .4rem; }
/* "Buying this for someone else?" - a native <details>, so the gift fields
   fold away with no script and the checkout underneath stays a plain form. */
.bs-gift { margin-top: 1.2rem; border-top: 1px solid var(--border); padding-top: 1rem; width: 100%; }
.bs-gift > summary { cursor: pointer; font-size: .86rem; text-transform: uppercase; letter-spacing: 1px; color: var(--violet); font-weight: 700; }
.bs-gift > summary::marker { color: var(--violet); }
.bs-gift label { display: flex; flex-direction: column; gap: .35rem; margin-top: .9rem; font-size: .86rem; color: var(--ink-soft); }
.bs-files { list-style: none; padding: 0; margin: .6rem 0 0; display: flex; flex-direction: column; gap: .4rem; width: 100%; }
.bs-files li { display: flex; justify-content: space-between; gap: 1rem; padding: .6rem .8rem; border: 1px solid var(--border); border-radius: 0; font-size: .86rem; }
.bs-badge { font-size: .86rem; text-transform: uppercase; letter-spacing: 1.5px; color: var(--violet); }
.fine-print { font-size: .86rem; opacity: .65; }
.rel-title { margin: .3rem 0 1rem; }

.hidden { display: none !important; }

/* purchases: table/grid view switch (backstage-app.js). Grid-first on a
   phone - a phone is bad at a horizontal table. */
.bs-view-toggle { display: flex; gap: .4rem; margin-bottom: 1rem; }
.bs-view-toggle [data-pview].active { background: var(--violet); color: #fff; border-color: var(--violet); }
.bs-pgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: 1rem; }
.bs-pgrid-card {
    display: flex; flex-direction: column; gap: .6rem;
    padding: 1rem 1.1rem; border: 1px solid var(--border); border-top: 2px solid var(--violet);
    background: var(--surface-strong);
}
.bs-pgrid-body { display: flex; flex-direction: column; gap: .3rem; }
.bs-pgrid-body strong { color: var(--silver); font-size: 1rem; }
.bs-pgrid-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; font-size: .86rem; color: var(--ink-soft); margin-top: .2rem; }

/* sortable table headers - same interaction as the dashboard's Files table
   (store.js sortTh()), an arrow drawn from the sorted column's data-dir
   rather than a JS-injected span, since here it's set directly on the <th>. */
.th-sort { cursor: pointer; user-select: none; white-space: nowrap; }
.th-sort:hover { color: var(--silver); }
.th-sort[data-dir="asc"]::after { content: ' \25B2'; font-size: .75em; color: var(--ink-soft); }
.th-sort[data-dir="desc"]::after { content: ' \25BC'; font-size: .75em; color: var(--ink-soft); }

/* ---------- shared with the dashboard, byte-identical values: the avatar
   cropper (public_html/avatar-cropper.js) and the modal/toast it opens
   into. sitePage() never loads dashboard/main.css, so these are duplicated
   here rather than linked - same convention as the shared input rule in
   theme.css. icon-btn deviates from theme.css's own copy by exactly one
   value (border-radius: 0, not 6px) to match this page's square corners;
   theme.css isn't loaded here either, so there's no rule to conflict with. */
.crop-viewport {
    position: relative; width: 220px; height: 220px; margin: 0 auto;
    overflow: hidden; border-radius: 0; background: var(--input-bg);
    cursor: grab; touch-action: none; user-select: none;
}
.crop-viewport:active { cursor: grabbing; }
.crop-img { position: absolute; max-width: none; }
.crop-ring { position: absolute; inset: 0; border-radius: 0; box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .7); pointer-events: none; }
.crop-zoom-row { display: flex; align-items: center; gap: .6rem; width: 100%; }
.crop-zoom-row input[type=range] { flex: 1; accent-color: var(--violet); }
.crop-hint { text-align: center; font-size: .86rem; margin: 0; }

.modal {
    position: fixed; inset: 0; background: rgba(5, 4, 7, .72);
    display: flex; align-items: flex-start; justify-content: center;
    padding: 4vh 1rem 2rem; z-index: 900; overflow: auto;
}
.modal-box {
    background: var(--ink); border: 1px solid rgba(138, 43, 226, .45); border-top: 3px solid var(--violet);
    border-radius: 0; width: 34rem; max-width: 100%; padding: 1.15rem 1.35rem 1.35rem;
    box-shadow: 0 24px 70px rgba(0, 0, 0, .6);
}
/* The header stays put while the media scrolls under it, so the way out is
   never scrolled off the top of a tall image. */
.modal-box header {
    display: flex; align-items: center; justify-content: space-between; gap: .8rem;
    margin-bottom: .9rem; position: sticky; top: 0; z-index: 2;
    background: var(--ink); padding-bottom: .6rem;
}
.modal-box header h2 { font-size: 1.05rem; line-height: 1.3; min-width: 0; overflow-wrap: anywhere; }
.modal-box.wide { width: 46rem; }

/* viewing a purchased file in place. The media is token-gated and served
   with content-disposition: inline (server.js's /d/ route) - never from
   /a/*, which is public and immutable-cached (CLAUDE.md rule 5). */
.bs-viewer-stage {
    display: flex; align-items: center; justify-content: center;
    min-height: 12rem; max-height: 62vh; overflow: hidden;
    background: var(--input-bg); border: 1px solid var(--border);
}
/* Default is always FIT: a 4K still has no business painting at 1:1 inside
   a phone. object-fit keeps the aspect ratio while it shrinks. */
.bs-viewer-media { max-width: 100%; max-height: 62vh; display: block; object-fit: contain; }
/* ...and "Actual size" is the opt-in, which turns the stage into the thing
   you pan around rather than letting the image push the page sideways. */
.bs-viewer-stage.zoomed { overflow: auto; align-items: flex-start; justify-content: flex-start; -webkit-overflow-scrolling: touch; }
.bs-viewer-stage.zoomed .bs-viewer-media { max-width: none; max-height: none; object-fit: none; }
audio.bs-viewer-media { width: 100%; max-width: 30rem; margin: 1.5rem; }
.bs-viewer-caption { margin: .7rem 0 0; font-size: .86rem; word-break: break-word; }
.bs-pgrid-acts { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.bs-pgrid-acts form { flex: 1 1 auto; }
.bs-table .acts { display: flex; gap: .4rem; justify-content: flex-end; align-items: center; }

.icon-btn {
    border: 1px solid var(--border); background: transparent; color: var(--silver);
    border-radius: 0; width: 1.9em; height: 1.9em; cursor: pointer; font-size: 1em; line-height: 1;
}
.icon-btn:hover { border-color: var(--violet); color: var(--violet); background: var(--violet-pale); }
/* The way out of a full-screen image has to be a real thumb target, not a
   14px glyph dwarfed by whatever it is sitting on top of. 2.75rem clears the
   44px minimum every touch guideline asks for. */
#modal-close {
    flex: 0 0 auto; width: 2.75rem; height: 2.75rem;
    font-size: 1.5rem; line-height: 1;
    border-color: var(--violet); color: var(--silver);
    display: flex; align-items: center; justify-content: center;
}

/* On a phone the viewer is the whole screen - a 34rem dialog floating in a
   412px viewport wasted most of it on padding. */
@media (max-width: 760px) {
    /* Full-bleed, but starting BELOW the site header: a dialog that fills a
       phone screen edge to edge otherwise buries the brand mark, and the
       modal's own sticky title bar lands exactly where the logo was. The
       header stays visible (and usable) above it. */
    .modal { padding: 0; top: var(--site-header-h); }
    .modal-box, .modal-box.wide {
        width: 100%; max-width: 100%; min-height: calc(100vh - var(--site-header-h));
        border-left: none; border-right: none; border-bottom: none;
        padding: .8rem 1rem 1.6rem;
    }
    .modal-box.viewer .bs-viewer-stage { max-height: 64vh; }
    .modal-box.viewer .bs-viewer-media { max-height: 64vh; }
    .modal-box .form-actions { flex-wrap: wrap; }
    .modal-box .form-actions .btn { flex: 1 1 auto; text-align: center; }
}

.toast {
    position: fixed; left: 50%; bottom: 1.5rem; transform: translateX(-50%);
    background: var(--bar); border: 1px solid var(--violet); color: var(--silver);
    border-radius: 0; padding: .6em 1.5em; box-shadow: 0 10px 30px rgba(0, 0, 0, .5);
    z-index: 950; font-size: .86rem; max-width: 92vw; text-align: center;
}

.arc-mast { padding: 70px 20px 10px; }
.arc-grid, .lore-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 26px; padding: 30px 20px 60px; max-width: 1400px; margin: 0 auto; }
/* --surface is rgba(255,255,255,.05) - 5% white, i.e. very nearly
   transparent. Over the site's own background media (the public pages all
   render on top of a full-bleed image) that made every card a ghost: the
   art read straight through the card and the text sat on whatever happened
   to be behind it. House style has always said a public-side card is
   var(--surface-strong); this rule just never followed it. */
.arc-card, .lore-card { display: flex; flex-direction: column; border: 1px solid rgba(138,43,226,.35); background: var(--surface-strong); transition: transform .3s ease, box-shadow .3s ease; }
.arc-card:hover, .lore-card:hover { transform: translateY(-3px); box-shadow: 0 6px 14px rgba(0,0,0,.3); }
.arc-cover, .lore-cover { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block; border-bottom: 1px solid rgba(138,43,226,.3); }
.arc-cover.none, .lore-cover.none { display: flex; align-items: center; justify-content: center; color: var(--silver); opacity: .35; font-size: .8rem; text-transform: uppercase; letter-spacing: 2px; }
.arc-body, .lore-card-body { padding: 1rem 1.1rem 1.2rem; display: flex; flex-direction: column; gap: .4rem; flex: 1; }
.arc-body h2, .lore-card-body h2 { font-size: 1.2rem; margin: 0; color: var(--silver); }
.arc-body time { font-size: .68rem; text-transform: uppercase; letter-spacing: 2px; color: var(--soulskin); }
/* .arc-card/.lore-card is itself an <a> - without an explicit color here
   this <p> inherits the global a{color:var(--violet)} rule at .75 opacity,
   which renders as barely-legible dark purple on --ink. The <h2> above only
   ever looked right because it already set color explicitly; this is the
   same fix applied to the line under it. */
.arc-body p, .lore-card-body p { font-size: .86rem; color: var(--silver); opacity: .8; line-height: 1.5; margin: 0; }
.arc-cta { margin-top: auto; padding-top: .8rem; font-size: .72rem; text-transform: uppercase; letter-spacing: 2px; color: var(--violet); }
.arc-none, .lore-none { padding: 60px 20px; }
.arc-back { display: inline-block; margin: 20px 0 0 20px; font-size: .72rem; text-transform: uppercase; letter-spacing: 2px; }

.rel-wrap { display: flex; gap: 3rem; align-items: flex-start; padding: 60px 20px 20px; max-width: 1400px; margin: 0 auto; flex-wrap: wrap; }
.rel-cover-main { flex: 0 0 min(34vw, 430px); width: 100%; object-fit: cover; aspect-ratio: 1 / 1; border: 1px solid rgba(138,43,226,.4); }
.rel-cover-main.none { display: flex; align-items: center; justify-content: center; color: var(--silver); opacity: .35; text-transform: uppercase; letter-spacing: 2px; font-size: .8rem; }
.rel-info { flex: 1; min-width: 300px; }
.rel-info time { font-size: .7rem; text-transform: uppercase; letter-spacing: 2px; color: var(--soulskin); }
.prod-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 20px; padding: 20px 20px 70px; max-width: 1400px; margin: 0 auto; }
.prod-card { display: flex; flex-direction: column; gap: .35rem; padding: 16px; border: 1px solid rgba(138,43,226,.3); background: var(--surface); transition: transform .3s ease, box-shadow .3s ease; }
.prod-card:hover { transform: translateY(-3px); box-shadow: 0 6px 14px rgba(0,0,0,.3); }
.prod-card.pack { border-color: var(--soulskin); }
.prod-art { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border: 1px solid rgba(138,43,226,.25); }
.prod-art.none { display: flex; align-items: center; justify-content: center; color: var(--silver); opacity: .3; font-size: .7rem; text-transform: uppercase; letter-spacing: 2px; }
.prod-kind { font-size: .66rem; text-transform: uppercase; letter-spacing: 2px; color: var(--violet); }
.prod-card.pack .prod-kind { color: var(--soulskin); }
.prod-card h3 { font-size: 1.05rem; color: var(--silver); margin: 0; }
/* Same inherited-link-color bug as .arc-body p above - .prod-card is an <a>. */
.prod-card p { font-size: .78rem; color: var(--silver); opacity: .72; line-height: 1.5; flex: 1; }
.prod-price { font-size: 1.3rem; font-weight: 700; color: var(--silver); padding-top: .4rem; }
.prod-card .btn { text-align: center; margin-top: .5rem; padding: .6rem 1rem; font-size: .8rem; }
.prod-members { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; margin-bottom: .35rem; }
.prod-members img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border: 1px solid rgba(138,43,226,.2); }
.prod-count { font-size: .66rem; text-transform: uppercase; letter-spacing: 2px; color: var(--soulskin); }
.prod-none { padding: 30px 20px 80px; }

.contact-wrap {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 2rem 1rem;
}
.contact-panel {
    width: 100%;
    max-width: 38rem;
    background: var(--surface-strong);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 2.4rem 2rem;
    display: flex;
    flex-direction: column;
    gap: 1.1rem;
}
.contact-panel h1 {
    font-weight: 700;
    font-size: 1.55rem;
    letter-spacing: 3px;
    color: var(--silver);
    margin: 0 0 .2rem;
}
.contact-panel p.sub {
    font-size: .86rem;
    color: var(--ink-soft);
    margin: 0;
}
.contact-panel hr {
    border: none;
    border-top: 1px solid var(--border);
    margin: .4rem 0;
}
.contact-panel label {
    display: flex;
    flex-direction: column;
    gap: .35rem;
    font-size: .86rem;
    font-weight: 700;
    letter-spacing: .5px;
    color: var(--ink-soft);
}
.contact-panel input,
.contact-panel textarea {
    background: var(--input-bg);
    border: 1px solid var(--border);
    border-radius: 0;
    color: var(--silver);
    padding: .8em 1em;
    font-family: inherit;
    font-size: 1rem;
    width: 100%;
    transition: border-color .2s ease;
}
.contact-panel input:focus,
.contact-panel textarea:focus {
    outline: none;
    border-color: var(--violet);
    box-shadow: 0 0 0 2px var(--violet-glow);
}
.contact-panel textarea { resize: vertical; min-height: 8rem; line-height: 1.55; }
.contact-panel .hp { position: absolute; left: -9999px; opacity: 0; }
.contact-actions { display: flex; justify-content: flex-end; }
.contact-panel .sent-msg {
    color: var(--silver);
    line-height: 1.6;
}
.contact-panel .sent-msg strong { color: var(--violet); }
.error { color: var(--danger); font-size: .88rem; }
.contact-back { align-self: flex-start; margin-top: .4rem; }
.contact-fields { display: flex; flex-direction: column; gap: 1.1rem; }
.contact-actions { display: flex; justify-content: flex-end; gap: .75rem; }

/* contact modal: the same panel, opened over whatever page embeds it via
   the #contact-modal fragment - no client JS, so open, close and cancel
   are all plain links toggling :target. */
.modal-backdrop { position: absolute; inset: 0; z-index: 1; }
.modal-veil {
    display: none;
    position: fixed; inset: 0;
    background: rgba(0,0,0,.75);
    align-items: center; justify-content: center;
    padding: 2rem 1rem;
    z-index: 1000;
}
#contact-modal:target { display: flex; }
.modal-veil .contact-panel { position: relative; z-index: 2; max-height: 90vh; overflow-y: auto; }

/* height (not just max-height) so every hero is the same box regardless of
   the source image's own proportions - a max-height alone lets a wide
   upload's auto-computed height collapse to a thin strip instead of
   matching a portrait upload's full-height box. object-fit:cover crops
   whatever comes in to fill it, so an oversize upload never distorts the
   layout either way. */
.lore-track-hero { width: 100%; height: min(60vh, 520px); object-fit: cover; display: block; border: 1px solid rgba(138,43,226,.35); margin-bottom: 1.6rem; }
.lore-track-hero.none { display: none; }
.lore-track-body { max-width: 42rem; margin: 0 auto; padding-bottom: 60px; }
.lore-kicker { color: var(--soulskin); font-size: .86rem; text-transform: uppercase; letter-spacing: 2px; margin-bottom: .6rem; }
.lore-summary { font-size: 1.1rem; opacity: .85; margin: .6rem 0 1.4rem; }
.lore-teaser, .lore-extended { line-height: 1.7; white-space: pre-wrap; }
.lore-extended { margin-top: 1.6rem; padding-top: 1.4rem; border-top: 1px solid var(--border); }
.lore-gate { margin-top: 1.6rem; padding: 1.4rem 1.6rem; border: 1px solid var(--border); background: var(--surface); display: flex; flex-direction: column; gap: .8rem; align-items: flex-start; }
.lore-gate p { margin: 0; font-size: .86rem; opacity: .8; }
.lore-gate-acts { display: flex; gap: .6rem; flex-wrap: wrap; }
.lore-entities { margin-top: 1.6rem; display: flex; flex-wrap: wrap; gap: .5rem; }
.lore-entities a.badge { text-decoration: none; }
.lore-entity-kind { color: var(--soulskin); font-size: .86rem; text-transform: uppercase; letter-spacing: 2px; }

/* track page: prev/next within the album */
.lore-track-nav { display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem; padding-top: 1.4rem; border-top: 1px solid var(--border); }
.lore-track-nav a { font-size: .86rem; color: var(--silver); text-decoration: none; opacity: .8; }
.lore-track-nav a:hover { opacity: 1; color: var(--violet); }
.lore-track-nav-next { text-align: right; }

/* glossary entry: grouped by volume, cross-volume span stated outright */
.lore-span { font-size: .86rem; opacity: .75; margin: .8rem 0 1.4rem; }
.lore-entity-group { margin-top: 1.4rem; }
.lore-entity-group-title { font-size: .86rem; color: var(--soulskin); text-transform: uppercase; letter-spacing: 2px; margin: 0 0 .5rem; }
.lore-entity-group .lore-entities { margin-top: 0; }

/* index: the glossary lives in the shared .rail (render.js's css(), same
   rail the dashboard and account hub use - already reaches /lore, already
   what this page's wide:'shell' is shaped for). .lore-entity-chip is the
   one thing of its own a rail entry needs beyond .rail-item: no underline,
   and the click-to-highlight active state. The filter input is a bare
   <input type="search"> with no class of its own for the same reason
   .lore-card is not a bespoke vocabulary (Rule B, CLAUDE.md) - it already
   gets the shared text-input rule (css(), below) every input on the site
   does. */
.lore-entity-chip { text-decoration: none; }
.lore-entity-chip.active { background: var(--violet); border-color: var(--violet); color: #fff; }

/* Volume / Album / Track all render as the same card, so each one says what
   it is. Same soulskin kicker treatment the track page already uses, with a
   matching feather icon (lore.js's LORE_ICON). */
.lore-type {
    display: flex; align-items: center; gap: .4rem; margin: 0;
    color: var(--soulskin); font-size: .86rem;
    text-transform: uppercase; letter-spacing: 2px;
}
.lore-type-ico { display: inline-flex; flex: 0 0 auto; }
/* 1.15em, not 1em: a 24-unit stroke icon set beside .86rem uppercase text
   optically reads smaller than the caps next to it. */
.lore-type-ico svg { width: 1.15em; height: 1.15em; display: block; }
.lore-track-name { display: flex; align-items: center; gap: .5rem; min-width: 0; }
.lore-track-name .lore-type-ico { color: var(--soulskin); opacity: .8; }

/* Card text must never run out of the bottom of its own card. The cover is
   a 1:1 square, so on a fixed-height card the text gets whatever is left -
   and a long blurb simply overran the edge. Both lines are clamped, so a
   card is the same box whatever the copy length. */
.lore-card { overflow: hidden; }
.arc-body h2, .lore-card-body h2 {
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    line-clamp: 2; overflow: hidden;
}
/* :not(.lore-type) matters - the type tag is itself a <p>, and this rule
   (class + element) outspecifies .lore-type's own display:flex, so without
   the exclusion the tag became a -webkit-box and its icon/label gap
   collapsed. */
.arc-body p:not(.lore-type), .lore-card-body p:not(.lore-type) {
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
    line-clamp: 3; overflow: hidden;
}

/* The deck (renderLoreIndex, lore.js): Volume and Album cards keep the
   shared .lore-card/.lore-grid look; these are only the wrappers and the
   track list that structure has to hold. This baseline (a plain grid, every
   Volume and Album expanded, every Track a real link) is what a no-JS
   visitor gets and always will - the stacked-deck/carousel rules below are
   scoped entirely under #lore-deck.lore-deck--js, a class only Phase 4's
   lore-app.js adds once it confirms it's actually running. Until that JS
   ships, this block is inert by construction: no selector in it can ever
   match, so today's page is unchanged. */
.lore-deck-volume { margin-bottom: 2.2rem; }
.lore-deck-volume > .lore-card { margin-bottom: 1rem; }
.lore-deck-albums { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 20px; padding-left: 1.2rem; border-left: 2px solid var(--border); }
.lore-deck-album > .lore-card { margin-bottom: .6rem; }
.lore-deck-tracks { display: flex; flex-direction: column; }
.lore-deck-track {
    display: flex; justify-content: space-between; align-items: center; gap: 1rem;
    padding: .6rem 0; font-size: .86rem; color: var(--silver); text-decoration: none;
    border-top: 1px solid var(--border);
}
.lore-deck-track:hover { color: var(--violet); }
/* A glossary chip click highlights the exact Track it appears in
   (public_html/lore-app.js) - restrained per house style, same active-state
   treatment as the chip itself, no glow. */
.lore-deck-track.lore-highlight { color: var(--violet); border-color: var(--violet); }

/* ---- the animated stage, JS-gated (lore-app.js adds .lore-deck--js once
   it confirms it's running - a no-JS visitor never reaches any rule below,
   same convention as before: [hidden] holds even if this stylesheet is
   stale or missing).

   Structure: exactly ONE level's row is ever live (laid out + visible) at
   a time - the rest of the tree stays in its real nested position, just
   [hidden], until selected. Volumes start as the live row (#lore-deck's
   own direct children); selecting one shrinks it to a marker and reveals
   its OWN nested .lore-deck-albums as the new live row (still nested
   exactly where it always was in the DOM - no reparenting, so no
   coordinate system ever has to cross a nesting boundary); selecting an
   Album the same way unfolds its OWN nested .lore-deck-tracks. Every
   sibling at whatever level isn't current gets .is-away and steps out
   entirely, so a taller focused card (once its tracklist unfolds) never
   fights shorter neighbours for space.

   Horizontal centering is real native scroll (scroll-snap + JS-driven
   scrollIntoView/scrollTo) - free keyboard, wheel, trackpad and drag
   support, and the actual fix for the "too fast, all at once" complaint:
   native scroll settles at its own pace instead of every card teleporting
   in lockstep. Each card's rotateY/scale/opacity by distance-from-focus,
   and the tracklist's fold-open, are computed and staggered by
   lore-app.js via element.animate() - never written into style="" (CSP
   style-src 'self', and the house rule that predates it - see CLAUDE.md).
   This CSS supplies the static scaffolding that runs on: card tokens, the
   scroll-snap row mechanics, the marker/away states, and a resting
   (collapsed) tracklist with its scrollbar hidden before JS ever touches
   it - a genuinely native scroll container throughout, drag/wheel/keyboard
   all real, none of it reimplemented. */
#lore-deck.lore-deck--js {
    --lore-card-w: min(300px, 78vw);
    /* taller than the 300px square cover leaves room for the type tag,
       a two-line title and a clamped blurb without any of it overrunning
       the card - the cover is 1:1, so whatever it doesn't take is all the
       text ever gets. */
    --lore-card-h: 480px;
}
/* The stage is a real tab stop (lore-app.js sets tabindex once it runs), so
   the browser draws its default focus ring on it - a hard 1px white box
   around the whole stage the moment an arrow key was pressed. Keep a
   visible indicator, because losing keyboard focus entirely is worse, but
   make it the house violet and only for keyboard focus. */
#lore-deck.lore-deck--js:focus { outline: none; }
#lore-deck.lore-deck--js:focus-visible {
    outline: 2px solid var(--violet); outline-offset: 4px;
}
#lore-deck.lore-deck--js,
#lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-deck-albums {
    display: flex; align-items: center; gap: 2.4rem;
    overflow-x: auto; scrollbar-width: none;
    scroll-snap-type: x mandatory;
    padding: 0 calc(50% - var(--lore-card-w) / 2);
    perspective: 1400px;
}
/* That padding is what centres card 0 at rest with no scroll needed - but
   #lore-deck itself keeps it even once a Volume is current and it holds
   only that one, now full-width, flex child. The padding doesn't go away
   just because the row's job changed, and it squeezed a 100%-width current
   Volume down to fit inside the sliver of content box the padding still
   left (exactly one card's worth - the same padding is why a Track link's
   own correct bounding box still resolved, on click, to the Volume's outer
   div: everything inside was crushed into that same narrow strip).
   #lore-deck stops being a coverflow row the moment it holds a current
   Volume, so it stops acting like one. */
#lore-deck.lore-deck--js:has(> .lore-deck-volume.is-current) {
    padding: 0; overflow-x: visible; scroll-snap-type: none;
    /* height:auto matters as much as the padding reset: the deck is a fixed
       --lore-card-h strip with align-items:center, so a drilled-in Volume
       taller than that strip (mobile stacks its marker above its row)
       overflowed equally out the TOP, riding up over the breadcrumb. */
    height: auto; align-items: stretch;
}
/* The Volumes row never has a child unfolding a tracklist beneath it, so it
   stays a fixed-height strip. A Volume's own Albums row can: its current
   Album grows a marker + tracklist inside it, which can genuinely run
   taller than one plain card - min-height (not height) plus visible
   overflow lets the row grow to fit that instead of clipping/scrolling it
   out of reach, which is exactly what happened here (a 5-track list plus
   its marker overran a hard-capped row height and got scrolled out of
   both view and click reach). */
#lore-deck.lore-deck--js { height: var(--lore-card-h); }
#lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-deck-albums {
    min-height: var(--lore-card-h); height: auto; overflow-y: visible;
}
#lore-deck.lore-deck--js::-webkit-scrollbar,
#lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-deck-albums::-webkit-scrollbar { display: none; }
/* scroll-snap-type: x mandatory reverts any programmatic scroll assignment
   to the nearest snap point - and drag-to-scroll works by assigning
   scrollLeft, so with snap left on a drag fought the browser every frame
   and travelled ~11px out of a 150px gesture. lore-app.js holds
   .is-dragging for the whole gesture INCLUDING its momentum tail, then
   drops it so the row settles onto a card rather than stopping mid-gap. */
#lore-deck.lore-deck--js.is-dragging,
#lore-deck.lore-deck--js .lore-deck-albums.is-dragging { scroll-snap-type: none; }
#lore-deck.lore-deck--js.is-dragging,
#lore-deck.lore-deck--js .is-dragging { user-select: none; cursor: grabbing; }
/* A row that can be dragged should say so before you try it. Only when it
   actually has somewhere to scroll - a single-card row is not draggable and
   should not pretend otherwise. */
#lore-deck.lore-deck--js,
#lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-deck-albums { cursor: grab; }
#lore-deck.lore-deck--js:has(> .lore-deck-volume.is-current) { cursor: default; }

/* No transform-style:preserve-3d here on purpose: each .lore-card is a flat
   leaf that gets its own rotateY/scale/translateZ and has no 3D-transformed
   children of its own, so it doesn't need a shared 3D coordinate space with
   its siblings - only the ROW (perspective, above) needs to exist. */
/* The no-JS baseline styles .lore-deck-albums as an indented grid with a
   border-left rule. In stage mode that has to be fully reset, not just
   overridden where convenient: left alone, every NON-current Volume's
   album grid still rendered, overflowing out the bottom of its own 420px
   card as a row of stray half-boxes, and its border-left painted as a
   stray vertical line beside the drilled-in row. Both were visible on the
   real page. A row only exists when its Volume is current. */
#lore-deck.lore-deck--js .lore-deck-albums {
    display: none; padding: 0; border: 0; margin: 0;
}
#lore-deck.lore-deck--js .lore-deck-volume,
#lore-deck.lore-deck--js .lore-deck-album {
    flex: 0 0 var(--lore-card-w);
    height: var(--lore-card-h);
    scroll-snap-align: center;
}
#lore-deck.lore-deck--js .lore-deck-volume > .lore-card,
#lore-deck.lore-deck--js .lore-deck-album > .lore-card {
    height: 100%; margin: 0; backface-visibility: hidden;
}
/* A card off to the side is a spectator until it's brought to focus - keeps
   keyboard/screen-reader focus from landing on something turned 45° away. */
#lore-deck.lore-deck--js .lore-deck-volume:not(.is-focused) > .lore-card,
#lore-deck.lore-deck--js .lore-deck-album:not(.is-focused) > .lore-card {
    pointer-events: none;
}
#lore-deck.lore-deck--js .lore-deck-volume.is-away,
#lore-deck.lore-deck--js .lore-deck-album.is-away { display: none; }

/* Most Lore rows have no cover art uploaded yet, and a 300x420 card whose
   top two thirds is an empty 1:1 placeholder is just a big hollow box with
   a title stranded at the bottom - which is exactly how the real site
   looked. When a card has no <img> cover, drop the placeholder entirely and
   let the card become typographic: title centred in the card, at a size
   that actually earns the space. A card WITH a cover is untouched. */
#lore-deck.lore-deck--js .lore-cover.none { display: none; }
#lore-deck.lore-deck--js .lore-card:not(:has(img.lore-cover)) {
    background: linear-gradient(160deg, rgba(138,43,226,.20), var(--surface-strong) 58%);
}
#lore-deck.lore-deck--js .lore-card:not(:has(img.lore-cover)) .lore-card-body {
    flex: 1; justify-content: center; gap: .7rem; padding: 1.6rem 1.4rem;
}
#lore-deck.lore-deck--js .lore-card:not(:has(img.lore-cover)) .lore-card-body h2 {
    font-size: 1.5rem; line-height: 1.25;
}

/* Drilled in: marker mode. The breadcrumb above already names where you
   are, so the marker is context, not content - a slim violet-spined label
   sitting beside (Volume) or above (Album) the row it opened, sized to its
   own text. It used to be a full-height 9rem-wide card, which rendered as
   a tall empty rectangle with the title jammed in the top corner: all the
   weight of a card, none of the content, and the single ugliest thing on
   the page. */
/* flex-start, not center: the Albums row beside this marker grows tall when
   one of its own Albums unfolds a tracklist, and a centred marker then
   floats halfway down the page detached from the row it labels. */
#lore-deck.lore-deck--js .lore-deck-volume.is-current {
    display: flex; align-items: flex-start; gap: 1.6rem;
    flex: 0 0 auto; width: 100%; height: var(--lore-card-h);
}
/* align-self overrides the row's own align-items:center, which otherwise
   vertically centres this (now much taller) column and drops its marker
   ~24px below the Volume marker beside it. Both markers label the same
   moment; they line up. */
#lore-deck.lore-deck--js .lore-deck-album.is-current {
    display: flex; flex-direction: column;
    flex: 0 0 var(--lore-card-w);
    height: auto; align-self: flex-start;
}
#lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-card,
#lore-deck.lore-deck--js .lore-deck-album.is-current > .lore-card {
    flex: 0 0 auto; height: auto; align-self: flex-start;
    background: transparent; border: 0; border-left: 2px solid var(--violet);
    padding: .2rem 0 .2rem .9rem;
}
#lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-card {
    width: 9rem; margin-top: .1rem;
}
#lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-card .lore-card-body,
#lore-deck.lore-deck--js .lore-deck-album.is-current > .lore-card .lore-card-body {
    padding: 0; flex: 0 0 auto;
}
#lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-card h2,
#lore-deck.lore-deck--js .lore-deck-album.is-current > .lore-card h2 {
    font-size: 1rem; letter-spacing: .5px;
}
/* A marker shows its title and nothing else. The blurb is real prose meant
   to be read at rest; the cover is actively harmful here - it's
   width:100% aspect-ratio:1/1 (ARCHIVE_CSS), so in the Album marker's
   column layout it computed square-to-full-card-width and overflowed down
   over the tracklist, swallowing clicks aimed at the Track links beneath. */
#lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-card .lore-card-body p,
#lore-deck.lore-deck--js .lore-deck-album.is-current > .lore-card .lore-card-body p,
#lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-card .lore-cover,
#lore-deck.lore-deck--js .lore-deck-album.is-current > .lore-card .lore-cover { display: none; }

/* The tracklist: starts collapsed (JS animates height 0 -> measured
   scrollHeight to fold it open); once open it's the real momentum-scroll
   surface - native drag/wheel/keyboard, scrollbar hidden, never trapped. */
#lore-deck.lore-deck--js .lore-deck-tracks {
    height: 0; overflow: hidden; margin: 0;
}
/* max-height caps the visible box regardless of what height the JS unfold
   animates to (the real content height, uncapped, so a short album's
   tracklist still reads its own natural size) - without this an Album with
   the owner's own stated average of ~11 tracks just grew tall enough to
   show all of them at once, and the drag/wheel scroll surface never
   actually had anything to scroll. Live-verified: at 11 tracks this now
   genuinely overflows and the momentum-scroll drag actually engages. */
#lore-deck.lore-deck--js .lore-deck-album.is-current .lore-deck-tracks {
    max-height: 320px;
    overflow-y: auto; scrollbar-width: none; margin-top: .8rem; cursor: grab;
}
#lore-deck.lore-deck--js .lore-deck-album.is-current .lore-deck-tracks::-webkit-scrollbar { display: none; }
#lore-deck.lore-deck--js .lore-deck-album.is-current .lore-deck-tracks.is-dragging { cursor: grabbing; }

@media (prefers-reduced-motion: reduce) {
    #lore-deck.lore-deck--js,
    #lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-deck-albums,
    #lore-deck.lore-deck--js .lore-deck-tracks {
        scroll-behavior: auto;
    }
}

@media (max-width: 760px) {
    #lore-deck.lore-deck--js { --lore-card-w: min(240px, 76vw); --lore-card-h: 340px; }
    /* A 9rem marker sitting beside the row costs a quarter of a 390px
       screen and squeezes the cards off the edge - stack it above instead,
       which also puts the marker directly over the row it labels. */
    #lore-deck.lore-deck--js .lore-deck-volume.is-current {
        flex-direction: column; align-items: stretch; gap: .9rem; height: auto;
    }
    #lore-deck.lore-deck--js .lore-deck-volume.is-current > .lore-card { width: auto; }
}

/* The search box and the breadcrumb had no vertical rhythm at all between
   them or the stage below - the three landed stacked flush against each
   other. These three margins are the whole fix. */
#lore-filter { margin-bottom: 1.5rem; }
.lore-crumb + #lore-deck { margin-top: 0; }

/* Breadcrumb (lore.js's .lore-crumb) - hidden server-side, unhidden by
   lore-app.js once the stage is live; a segment per drilled level. */
.lore-crumb { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; margin: 0 0 2rem; }
.lore-crumb-seg {
    background: none; border: none; padding: 0; margin: 0; font: inherit;
    color: var(--silver); opacity: .55; cursor: pointer;
    text-transform: uppercase; letter-spacing: 1.5px; font-size: .86rem;
}
.lore-crumb-seg[aria-current="true"] { opacity: 1; color: var(--violet); cursor: default; }
.lore-crumb-seg:hover:not([aria-current="true"]) { opacity: 1; color: var(--violet); }
.lore-crumb-sep { opacity: .35; font-size: .86rem; }

@media (max-width: 760px) {
    .lore-track-hero { height: min(40vh, 320px); }
    .lore-gate-acts { flex-direction: column; align-items: stretch; }
    .lore-gate-acts .btn { text-align: center; }
    .lore-deck-albums { grid-template-columns: 1fr; padding-left: .8rem; }
    .lore-deck-track { flex-direction: column; align-items: flex-start; gap: .2rem; }
    #lore-deck.lore-deck--js .lore-deck-volume.is-open { flex-direction: column; }
    #lore-deck.lore-deck--js .lore-deck-volume.is-open > .lore-card { order: 1; flex-basis: auto; max-width: none; }
    #lore-deck.lore-deck--js .lore-deck-volume.is-open .lore-deck-albums { order: 2; }
}
