/* TimelineTrack — one stylesheet, both themes.
 *
 * The palette is the brand's: the navy and coral of the approved mark, sampled
 * from the artwork itself rather than guessed at. The film-strip icon is navy
 * #06254b with a coral #fb7a61 strip and a cream highlight, and everything
 * below is built from those three.
 *
 * Two tiers, and the difference matters.
 *
 *   --site-navy and --site-coral are TimelineTrack's own, and a franchise can
 *   never touch them. They are the header, the mark, the wordmark: the parts of
 *   the page that are this site speaking.
 *
 *   --brand and --accent belong to whichever franchise you are looking at, and
 *   arrive in a small generated stylesheet (src/theme.js). Marvel is red here,
 *   Star Wars is blue. The values below are the neutral fallback for pages that
 *   belong to no franchise — the front page, the letter, the privacy page.
 *
 * Contrast: every text/ground pair clears WCAG AA in both themes, and the
 * ratios in the comments are measured. The worst is 5.24:1. The coral is
 * 2.37:1 on the cream ground and is therefore never text on it — it is a fill,
 * with navy on top at 5.86:1. In dark it reaches 6.83:1 and can be read.
 *
 * Corners are square. There is one border-radius in this file and it is 2px.
 */

:root {
    /* the brand, never overridden by a franchise */
    --site-navy:   #06254b;
    --site-coral:  #fb7a61;
    --site-cream:  #fdf3d7;

    /* ground */
    --bg:          #f7f4ed;
    --bg-2:        #efeae0;
    --surface:     #ffffff;
    --raised:      #eae4d8;
    --border:      #ded7c8;
    --border-firm: #c3b9a4;

    /* ink */
    --text:        #06254b;   /* 13.91:1 */
    --text-2:      #3c5877;   /*  6.70:1 */
    --text-3:      #4f6884;   /*  5.24:1 — the small print is still meant to be read */

    /* brand — overridden per franchise */
    --brand:       #fb7a61;   /*  2.37:1 — fills and rules only, never text */
    --accent:      #a8331f;   /*  6.05:1 */
    --accent-hi:   #8a2818;   /* hover and pressed */
    --on-accent:   #ffffff;

    --shadow: 0 1px 2px rgb(6 37 75 / .05), 0 10px 28px -14px rgb(6 37 75 / .18);

    --measure: 68ch;
    --page: 1080px;
    --radius: 2px;
    --radius-sm: 2px;

    --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
            "Helvetica Neue", Arial, sans-serif;
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg:          #06182f;
        --bg-2:        #0a2140;
        --surface:     #0c2547;
        --raised:      #12304f;
        --border:      #1b3c5e;
        --border-firm: #2c5279;

        --text:        #f2ece0;   /* 15.13:1 */
        --text-2:      #a9b6c6;   /*  8.64:1 */
        --text-3:      #8e9db0;   /*  6.44:1 */

        --brand:       #fb7a61;
        --accent:      #fd9a86;   /*  8.60:1 — the coral reads fine as text here */
        --accent-hi:   #ffb9aa;
        --on-accent:   #06182f;

        --shadow: 0 1px 2px rgb(0 0 0 / .4), 0 10px 28px -14px rgb(0 0 0 / .6);
    }
}

:root[data-theme="dark"] {
    --bg:      #06182f;
    --bg-2:    #0a2140;
    --surface:     #0c2547;
    --raised:      #12304f;
    --border:      #1b3c5e;
    --border-firm: #2c5279;

    --text:    #f2ece0;   /* 15.13:1 */
    --text-2:      #a9b6c6;   /*  8.64:1 */
    --text-3:      #8e9db0;   /*  6.44:1 */

    --brand:       #fb7a61;
    --accent:      #fd9a86;   /*  8.60:1 — the coral reads fine as text here */
    --accent-hi:   #ffb9aa;
    --on-accent:   #06182f;

    --shadow: 0 1px 2px rgb(0 0 0 / .4), 0 10px 28px -14px rgb(0 0 0 / .6);
}

/* --- base ---------------------------------------------------------------- */

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

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    scroll-padding-top: 5rem;   /* so an anchored entry clears the sticky header */
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font);
    font-size: 17px;
    line-height: 1.65;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

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

h1, h2, h3 { line-height: 1.2; letter-spacing: -.02em; margin: 0 0 .5em; }
h1 { font-size: clamp(1.9rem, 1.3rem + 2.4vw, 2.9rem); font-weight: 750; }
h2 { font-size: clamp(1.35rem, 1.1rem + 1vw, 1.7rem); font-weight: 700; }
h3 { font-size: 1.06rem; font-weight: 650; }
p  { margin: 0 0 1em; }
a  { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent-hi); }

:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    border-radius: 3px;
}

.wrap { max-width: var(--page); margin: 0 auto; padding: 0 20px; }
.prose { max-width: var(--measure); }
.sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip {
    position: absolute; left: -9999px; top: 0; z-index: 100;
    background: var(--accent); color: var(--on-accent);
    padding: 10px 16px; border-radius: 0 0 var(--radius-sm) 0; font-weight: 600;
}
.skip:focus { left: 0; }

.eyebrow {
    font-size: .74rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: .12em; color: var(--accent); margin: 0 0 .6em;
}

/* --- header -------------------------------------------------------------- */

.site-head {
    position: sticky; top: 0; z-index: 50;
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    backdrop-filter: saturate(180%) blur(14px);
    border-bottom: 1px solid var(--border);
}
@supports not (backdrop-filter: blur(1px)) { .site-head { background: var(--bg); } }

.site-head .wrap { display: flex; align-items: center; gap: 20px; min-height: 60px; }
.brand {
    display: inline-flex; align-items: center; gap: 9px;
    text-decoration: none; margin-right: auto; color: var(--text);
}
.brand:hover { color: var(--text); }
.brand-mark { width: 24px; height: 24px; flex: none; display: block; }

/* The wordmark, rebuilt in type so it follows the theme. "Track" is coral in
 * both, because that is the mark; the coral is a fill here, not small print. */
.wordmark {
    font-weight: 750; font-size: 1.06rem; letter-spacing: -.03em;
    color: var(--text);
}
.wordmark-track { color: var(--site-coral); }
:root:not([data-theme="dark"]) .wordmark-track { color: #bf3a24; }  /* 5.1:1 — the artwork coral is 2.4:1 and unreadable at this size */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .wordmark-track { color: var(--site-coral); }
}
:root[data-theme="dark"] .wordmark-track { color: var(--site-coral); }

.site-nav { display: flex; gap: 4px; flex-wrap: wrap; }
.site-nav a {
    color: var(--text-2); text-decoration: none; font-weight: 600; font-size: .93rem;
    padding: 7px 11px; border-radius: var(--radius-sm);
}
.site-nav a:hover { color: var(--text); background: var(--raised); }
.site-nav a[aria-current="page"] { color: var(--accent); background: var(--raised); }

/* --- switches (order + groups) ------------------------------------------- */
/* These are links, not buttons. Every combination is a real page at a real
 * address, which is what makes them work with JavaScript off and what makes
 * the address bar itself the shareable filtered link. */

.switches {
    background: var(--bg-2);
    border-bottom: 1px solid var(--border);
    padding: 14px 0;
}
.switch-row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.switch-row + .switch-row { margin-top: 10px; }
.switch-label {
    font-size: .74rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: .1em; color: var(--text-3); flex: none; min-width: 5.5em;
}
.switch-set { display: flex; gap: 6px; flex-wrap: wrap; }

.switch {
    display: inline-block; padding: 6px 13px; border-radius: var(--radius-sm);
    border: 1px solid var(--border-firm); background: var(--surface);
    color: var(--text-2); text-decoration: none;
    font-size: .88rem; font-weight: 600; line-height: 1.4;
}
.switch:hover { color: var(--text); border-color: var(--text-3); }
.switch[aria-pressed="true"], .switch[aria-current="true"] {
    background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}
.switch[aria-pressed="true"]:hover, .switch[aria-current="true"]:hover {
    background: var(--accent-hi); border-color: var(--accent-hi); color: var(--on-accent);
}
/* The last group cannot be switched off — an empty timeline is not a view. */
.switch[aria-disabled="true"] { opacity: .55; pointer-events: none; }

.switch-note { font-size: .85rem; color: var(--text-3); margin: 10px 0 0; }

/* --- the timeline -------------------------------------------------------- */

.timeline-head { padding: 28px 0 6px; }
.timeline-head p { color: var(--text-2); max-width: var(--measure); }
.tally { font-size: .9rem; color: var(--text-3); }
.tally strong { color: var(--text-2); font-weight: 650; }

.timeline {
    list-style: none; margin: 0; padding: 24px 0 48px;
    position: relative;
}
/* The rail. Amber, and purely a shape — see the note at the top of the file. */
/* The connectors run between the entries themselves, not as one rule down the
 * side of everything.
 *
 * A single long rail looks right until something is filtered out — the line
 * carries on through a gap where an entry used to be, and it runs past the end
 * of a section into whitespace. A segment per entry, drawn from under its own
 * number down to the next one, chains correctly however the list is filtered,
 * because a hidden entry takes its own piece of the line with it.
 */
.node { position: relative; padding-left: 50px; margin-bottom: 18px; }

/* The segment from under this entry's number down to the next one. */
.node::before {
    content: ""; position: absolute; left: 16px; top: 42px; bottom: -18px; width: 2px;
    background: var(--brand); opacity: .45;
}
/* Nothing hangs off the last one in a section. `:has` keeps this correct when
 * entries are filtered out, because the last VISIBLE node is the one that
 * matters, not the last one in the markup. */
.node:last-child::before { display: none; }
.node:not([hidden]):has(~ .node:not([hidden])) ::before { }
@supports selector(:has(*)) {
    .node:not([hidden])::before { display: block; }
    .node:not([hidden]):not(:has(~ .node:not([hidden])))::before { display: none; }
}

@media (max-width: 640px) {
    .node { padding-left: 0; }
    .node::before { display: none; }
}

/* The number is a CSS counter, not a number printed by the build.
 *
 * Every filter on this page — the path cards, the type buttons, the search box —
 * hides entries, and a printed number would then be wrong: you would search for
 * something and get items 3, 17 and 44. A counter over the entries that are
 * actually showing renumbers itself, for free, however the list is filtered.
 *
 * It is aria-hidden. Each section is a real <ol>, so a screen reader gets its
 * own position from the list rather than from generated content, and the two
 * can never contradict each other. */
main { counter-reset: node; }
.node:not([hidden]) { counter-increment: node; }
.node-num::before { content: counter(node); }

.node-num {
    position: absolute; left: 0; top: 16px;
    width: 34px; height: 26px;
    display: grid; place-items: center;
    background: var(--accent); color: var(--on-accent);
    font-weight: 700; font-size: .84rem; font-variant-numeric: tabular-nums;
}
@media (max-width: 640px) {
    .node-num { position: static; width: auto; height: auto; border: 0; background: none;
                color: var(--text-3); font-size: .8rem; display: inline; }
    .node-num::after { content: "."; }
}

.card {
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); padding: 18px 20px; box-shadow: var(--shadow);
    display: grid; grid-template-columns: 150px 1fr; gap: 18px;
}
@media (max-width: 560px) { .card { grid-template-columns: 1fr; gap: 12px; } }

.poster {
    width: 150px; aspect-ratio: 2 / 3; border-radius: var(--radius-sm);
    background: var(--raised); border: 1px solid var(--border);
    object-fit: cover; display: block;
}
.poster-none {
    display: grid; place-items: center; text-align: center;
    color: var(--text-3); font-size: .7rem; padding: 8px; line-height: 1.3;
}
@media (max-width: 560px) { .poster, .poster-none { width: 120px; } }

.card-body { min-width: 0; }
.card h3 { margin: 0 0 4px; font-size: 1.14rem; }
.card h3 a { color: var(--text); text-decoration: none; }
.card h3 a:hover { color: var(--accent); text-decoration: underline; }

.meta {
    display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 0 0 9px;
    font-size: .84rem; color: var(--text-3);
}
.meta .kind {
    text-transform: uppercase; letter-spacing: .08em; font-weight: 700; font-size: .72rem;
    color: var(--text-2);
}
.summary { margin: 0; color: var(--text-2); max-width: 62ch; }
.setting-note { margin: 7px 0 0; font-size: .84rem; color: var(--text-3); font-style: italic; }

.canon-note {
    margin: 9px 0 0; padding: 8px 11px; border-radius: var(--radius-sm);
    background: var(--raised); border-left: 3px solid var(--border-firm);
    font-size: .84rem; color: var(--text-2);
}

/* --- promoted post-credit beats ------------------------------------------ */
/* Visually a lighter thing than a film, because it is a two-minute scene, but
 * it holds a real position in the list rather than hanging off the card above. */

.node-beat .card {
    grid-template-columns: 1fr;
    background: var(--bg-2); border-style: dashed;
    box-shadow: none;
}
.node-beat .node-num { background: var(--raised); color: var(--text-2);
                       box-shadow: inset 0 0 0 1px var(--border-firm); }
.beat-kind {
    font-size: .72rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: .09em; color: var(--accent); margin: 0 0 4px;
}
.leads-to { margin: 9px 0 0; font-size: .88rem; }
.leads-to::before { content: "→ "; color: var(--text-3); }

/* --- spoilers ------------------------------------------------------------ */
/*
 * A blocked-out name is inside a `hidden` element, which is a user-agent
 * display:none rule. That means it is invisible to assistive technology AND to
 * find-in-page, which a `<details>` element is not: Chrome and Firefox both
 * auto-expand a closed <details> to show a Ctrl+F match, which would defeat the
 * entire point.
 *
 * The reveal is a sibling checkbox and a `~` combinator. No :has(), so this
 * works in very old browsers, and — the part that matters — if the stylesheet
 * fails to load for any reason, the name simply stays hidden. That is the safe
 * direction to fail.
 */

.reveal-box { position: absolute; width: 1px; height: 1px; opacity: 0; }

/* Redacted, and labelled so nobody has to guess it is clickable. A solid bar
 * with the instruction printed on it. */
.reveal-block {
    display: inline-block; vertical-align: baseline; cursor: pointer;
    background: var(--text); color: var(--bg);
    padding: 0 8px; font-size: .74rem; font-weight: 650; line-height: 1.75;
    letter-spacing: .03em; text-transform: uppercase;
    -webkit-user-select: none; user-select: none;
}
.reveal-block:hover { background: var(--accent); color: var(--on-accent); }
.reveal-box:focus-visible ~ .reveal-block { outline: 3px solid var(--accent); outline-offset: 2px; }

.reveal-box:checked ~ .reveal-block { display: none; }
.reveal-box:checked ~ .reveal-name[hidden] { display: inline; }

/* The global override, and the one the spoiler baseline drives. Both work by
 * flipping the same two rules, so there is one behaviour and not three. */
#show-all:checked ~ #main .reveal-block { display: none; }
#show-all:checked ~ #main .reveal-name[hidden] { display: inline; }
html[data-spoilers="all"] .reveal-block { display: none; }
html[data-spoilers="all"] .reveal-name[hidden] { display: inline; }

.reveal-name .actor { color: var(--text-3); }

/* --- cast ---------------------------------------------------------------- */

.cast-list {
    list-style: none; margin: 12px 0 0; padding: 0;
    display: flex; flex-wrap: wrap; gap: 5px 8px; font-size: .88rem;
}
.cast-list li { color: var(--text-2); }
.cast-list li::after { content: "·"; color: var(--border-firm); margin-left: 8px; }
.cast-list li:last-child::after { content: ""; }
.cast-list a { color: var(--text-2); text-decoration: none; }
.cast-list a:hover { color: var(--accent); text-decoration: underline; }

/* --- grades -------------------------------------------------------------- */

.grade {
    display: inline-grid; place-items: center; min-width: 2.1em; padding: 1px .35em;
    border-radius: var(--radius-sm); font-weight: 750; font-size: .9rem;
    font-variant-numeric: tabular-nums;
    background: var(--raised); color: var(--text); border: 1px solid var(--border-firm);
}
.grade-votes { font-size: .78rem; color: var(--text-3); }

/* --- generic sections, footer -------------------------------------------- */

section { padding: 44px 0; border-top: 1px solid var(--border); }
section:first-of-type { border-top: 0; }

.grid {
    display: grid; gap: 18px;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.tile {
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); padding: 20px;
}
.tile h3 { margin-top: 0; }
.tile p:last-child { margin-bottom: 0; }

.btn-row { display: flex; gap: 10px; flex-wrap: wrap; }
.btn {
    display: inline-block; padding: 10px 18px; border-radius: var(--radius-sm);
    font-weight: 650; text-decoration: none; border: 1px solid transparent;
}
.btn-primary { background: var(--accent); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-hi); color: var(--on-accent); }
.btn-ghost { border-color: var(--border-firm); color: var(--text-2); }
.btn-ghost:hover { border-color: var(--text-3); color: var(--text); }

.site-foot {
    border-top: 1px solid var(--border); background: var(--bg-2);
    padding: 34px 0; margin-top: 40px; font-size: .9rem; color: var(--text-3);
}
.foot-nav { display: flex; gap: 16px; flex-wrap: wrap; margin: 0 0 14px; }
.foot-nav a { color: var(--text-2); text-decoration: none; font-weight: 600; }
.foot-nav a:hover { color: var(--accent); text-decoration: underline; }
.foot-legal { margin: 0; max-width: var(--measure); }
.foot-legal + .foot-legal { margin-top: 8px; }

/* --- the letter (about) --------------------------------------------------- */

.letter { max-width: 62ch; margin: 0 auto; }
.letter-greeting { font-size: 1.7rem; margin-bottom: .8em; }
.letter-body p { color: var(--text); }
.letter-signoff { margin-top: 30px; padding-left: min(58%, calc(100% - 260px)); }
.signature-mark { width: 190px; height: auto; color: var(--text); display: block; }

/* --- plain lists (character pages, indexes) ------------------------------ */

.plain-list { list-style: none; margin: 0; padding: 0; }
.plain-list li {
    padding: 9px 0; border-bottom: 1px solid var(--border);
    display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
}
.plain-list li:last-child { border-bottom: 0; }
.plain-list a { font-weight: 600; }
.meta-inline { font-size: .84rem; color: var(--text-3); }
.badge {
    font-size: .7rem; text-transform: uppercase; letter-spacing: .08em; font-weight: 700;
    color: var(--text-3); border: 1px solid var(--border-firm);
    border-radius: var(--radius-sm); padding: 0 7px;
}
.columns { columns: 2; column-gap: 32px; }
.columns li { break-inside: avoid; }
@media (max-width: 620px) { .columns { columns: 1; } }

/* --- the global spoiler switch ------------------------------------------- */
/* The checkbox itself sits before <main> so the ~ combinator can reach the
 * whole page. It is never visible; the label below is the control. */

.sr-switch { position: absolute; width: 1px; height: 1px; opacity: 0; }

.switch-reveal { cursor: pointer; user-select: none; }
.sr-switch:checked ~ * .switch-reveal,
.sr-switch:checked ~ .switch-reveal {
    background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}
.sr-switch:focus-visible ~ * .switch-reveal { outline: 3px solid var(--accent); outline-offset: 2px; }
.switch-note.inline { margin: 0; }

.inline-switches {
    background: transparent; border: 0; padding: 0 0 14px;
}

/* --- post-credit scenes on a card ---------------------------------------- */

.stinger-list {
    list-style: none; margin: 12px 0 0; padding: 10px 0 0;
    border-top: 1px dashed var(--border-firm);
    font-size: .88rem; color: var(--text-2);
}
.stinger-list li + li { margin-top: 7px; }
.beat-kind-inline {
    font-size: .7rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: .08em; color: var(--accent); margin-right: 6px;
}
.beat-jump { font-size: .84rem; white-space: nowrap; }

/* --- the front page ------------------------------------------------------
 *
 * Just the franchises. The default here is a plain centred row that needs no
 * JavaScript and never moves; assets/orbs.js adds `is-floating` and takes over
 * positioning only when it is running, which it will not do if the reader has
 * asked for reduced motion.
 */

.orb-field {
    display: flex; flex-wrap: wrap; gap: 28px;
    align-items: center; justify-content: center;
    min-height: calc(100vh - 190px);
    padding: 24px 0;
}

/* Once the script is driving, the tiles are placed by transform. */
.orb-field.is-floating { display: block; position: relative; }
.orb-field.is-floating .orb-tile { position: absolute; top: 0; left: 0; margin: 0; }

.orb-tile {
    display: grid; justify-items: center; gap: 4px;
    padding: 8px; text-decoration: none; color: var(--text);
    width: 240px;
}
.orb-tile:hover { color: var(--text); }

/* --- the orb ------------------------------------------------------------
 *
 * A glass ball with a world inside it. Three layers: the artwork, an inner
 * shadow so it reads as being down inside a sphere rather than pasted flat, and
 * the glass itself over the top.
 *
 * `overflow: hidden` on a circle is what guarantees nothing of the artwork ever
 * shows outside the ball, whatever shape the source image was.
 */

.orb {
    position: relative; display: block;
    width: 220px; height: 220px; border-radius: 50%;
    overflow: hidden;
    isolation: isolate;
}

/* Before the artwork exists: the franchise colour and its drawn glyph. */
.orb:not(.has-world) {
    background:
        radial-gradient(circle at 34% 28%,
            color-mix(in srgb, var(--brand) 55%, transparent) 0%,
            color-mix(in srgb, var(--brand) 20%, transparent) 45%,
            transparent 72%),
        var(--raised);
    box-shadow: inset 0 0 0 1px var(--border-firm);
}
.orb-glyph {
    position: absolute; inset: 0; display: grid; place-items: center;
    color: var(--brand);
}
.orb-glyph svg { width: 108px; height: 108px; }

/* The world. Slightly larger than the ball so the crop never leaves a hairline
 * of background at the rim. */
.orb-world {
    position: absolute; inset: -1px;
    width: calc(100% + 2px); height: calc(100% + 2px);
    object-fit: cover; display: block;
}

/* Depth. A dark ring on the inside edge and a light lift at the top left, which
 * is what stops a flat picture looking like a sticker. */
.orb-depth {
    position: absolute; inset: 0; border-radius: 50%; pointer-events: none;
    background:
        radial-gradient(circle at 32% 26%,
            rgb(255 255 255 / .30) 0%, rgb(255 255 255 / .06) 26%, transparent 48%),
        radial-gradient(circle at 50% 50%,
            transparent 55%, rgb(2 10 24 / .28) 82%, rgb(2 10 24 / .55) 100%);
}

/* The glass, over everything. */
.orb-glass {
    position: absolute; inset: 0; width: 100%; height: 100%;
    display: block; pointer-events: none;
}

.orb-tile:hover .orb, .orb-tile:focus-visible .orb {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 70%, transparent);
}
.orb-tile:hover .orb-glyph, .orb-tile:focus-visible .orb-glyph { color: var(--accent); }
.orb-tile:focus-visible { outline: 3px solid var(--accent); outline-offset: 6px; }

.orb-name { margin-top: 10px; font-weight: 700; font-size: 1rem; letter-spacing: -.015em; text-align: center; }
.orb-meta { font-size: .82rem; color: var(--text-3); }

@media (prefers-reduced-motion: no-preference) {
    .orb-tile, .orb { transition: color .15s ease, box-shadow .15s ease; }
}

/* --- watch tracking, grades, progress ------------------------------------ */
/* None of this exists until site.js builds it, so there is nothing here that
 * a person without JavaScript can see and fail to use. */

.node-actions {
    display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
    margin-top: 12px;
}
.node-actions:empty { display: none; }

.mark-button, .theme-button {
    font: inherit; font-size: .8rem; font-weight: 600; line-height: 1.5;
    padding: 4px 11px; cursor: pointer;
    background: var(--surface); color: var(--text-2);
    border: 1px solid var(--border-firm); border-radius: var(--radius-sm);
}
.mark-button:hover, .theme-button:hover { color: var(--text); border-color: var(--text-3); }
.mark-button[aria-pressed="true"] {
    background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}

.grade-select {
    font: inherit; font-size: .8rem; font-weight: 600;
    padding: 4px 8px; cursor: pointer;
    background: var(--surface); color: var(--text-2);
    border: 1px solid var(--border-firm); border-radius: var(--radius-sm);
}

/* A watched entry is dimmed slightly, never hidden. It is still part of the
 * list and still numbered; you have simply been there. */
.node.is-watched .card { border-color: var(--border); background: var(--bg-2); }
.node.is-watched .node-num { opacity: .55; }

.progress {
    margin: 0 0 6px; font-size: .86rem; color: var(--text-3);
}

.save-warning {
    margin: 16px 0; padding: 11px 14px; font-size: .88rem;
    background: var(--raised); border-left: 3px solid var(--brand); color: var(--text-2);
}

/* A block the baseline opened for you looks different from one you clicked:
 * the site is telling you it decided, not that you did. */
.cast-spoiler.is-past .reveal-name { opacity: .82; }

/* --- sign in -------------------------------------------------------------- */

.signin-form {
    display: grid; gap: 8px; justify-items: start;
    max-width: 30rem; margin: 0 0 32px;
}
.signin-form label { font-size: .84rem; font-weight: 650; color: var(--text-2); }
.signin-form input[type="email"] {
    font: inherit; width: 100%;
    padding: 9px 12px; background: var(--surface); color: var(--text);
    border: 1px solid var(--border-firm); border-radius: var(--radius-sm);
}
.signin-form input[type="email"]:focus-visible {
    outline: 3px solid var(--accent); outline-offset: 1px; border-color: var(--accent);
}

/* --- grading the cast ---------------------------------------------------- */

.cast-grades {
    margin-top: 10px; padding: 10px 12px;
    background: var(--bg-2); border: 1px solid var(--border);
    display: grid; gap: 6px;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
.cast-grade-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px; font-size: .86rem; color: var(--text-2);
}
.cast-grade-row .grade-select { flex: none; }

.cast-note {
    margin: 12px 0 0; font-size: .84rem; color: var(--text-3); font-style: italic;
}

/* --- picking a path ------------------------------------------------------ */

.franchise-head {
    display: flex; align-items: center; gap: 28px; flex-wrap: wrap;
    padding-bottom: 20px; border-top: 0;
}
.franchise-orb .orb { width: 150px; height: 150px; }
.franchise-head h1 { margin-bottom: .3em; }
.franchise-head p { color: var(--text-2); margin: 0; }

.path-cards {
    display: grid; gap: 14px;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.path-card {
    display: grid; gap: 5px; align-content: start;
    padding: 20px; text-decoration: none; color: var(--text);
    background: var(--surface); border: 1px solid var(--border);
    border-left: 4px solid var(--border-firm);
}
.path-card:hover, .path-card:focus-visible {
    border-color: var(--accent); border-left-color: var(--accent); color: var(--text);
}
.path-card.is-current {
    border-color: var(--accent); border-left-color: var(--accent);
    background: var(--raised);
}
.path-card.is-current .path-label { color: var(--accent); }
/* The cards sit in the switch bar on a timeline, so they need to be tighter
 * than they were on a page of their own. */
.path-band {
    background: var(--bg-2); border-bottom: 1px solid var(--border);
    padding: 20px 0;
}
.path-heading {
    margin: 0 0 12px; font-size: .78rem; font-weight: 750;
    text-transform: uppercase; letter-spacing: .12em; color: var(--text-3);
}
.path-band .path-card { padding: 14px 16px; gap: 3px; }
.path-band .path-label { font-size: .98rem; }
.path-band .path-blurb { font-size: .85rem; }
.path-band .path-numbers { margin-top: 3px; font-size: .78rem; }
.path-label { font-weight: 750; font-size: 1.06rem; letter-spacing: -.02em; }
.path-blurb { color: var(--text-2); font-size: .92rem; }
.path-numbers { margin-top: 6px; font-size: .84rem; color: var(--text-3); }
.path-numbers strong { color: var(--text-2); font-weight: 650; }

.path-crumb { margin: 0 0 12px; font-size: .84rem; color: var(--text-3); }
.path-crumb a { color: var(--text-2); }
.path-crumb span { margin: 0 5px; }

/* --- the numbers --------------------------------------------------------- */

.stats {
    list-style: none; margin: 18px 0 0; padding: 0;
    display: grid; gap: 10px;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.stats li {
    background: var(--surface); border: 1px solid var(--border);
    padding: 12px 14px; display: grid; gap: 2px;
}
.stats strong {
    font-size: 1.5rem; font-weight: 750; color: var(--accent);
    font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
.stats span {
    font-size: .68rem; text-transform: uppercase; letter-spacing: .1em;
    color: var(--text-3); font-weight: 650;
}

/* --- search and the type filter ------------------------------------------ */

.search-field { flex: 1 1 200px; min-width: 160px; }
.search-field input {
    font: inherit; font-size: .88rem; width: 100%;
    padding: 6px 11px; background: var(--surface); color: var(--text);
    border: 1px solid var(--border-firm); border-radius: var(--radius-sm);
}
.search-field input:focus-visible {
    outline: 3px solid var(--accent); outline-offset: 1px; border-color: var(--accent);
}
button.switch { font: inherit; cursor: pointer; }
button.switch[aria-pressed="true"] {
    background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}

/* --- eras ---------------------------------------------------------------- */

.era { padding: 26px 0 0; border-top: 0; }
.era-head {
    display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
    margin: 0 0 4px; padding-bottom: 8px; border-bottom: 1px solid var(--border);
    font-size: .82rem; font-weight: 750; text-transform: uppercase; letter-spacing: .12em;
    color: var(--accent);
}
.era-count {
    font-size: .72rem; letter-spacing: .06em; color: var(--text-3);
    font-weight: 600; white-space: nowrap;
}
.era-blurb { margin: 8px 0 0; font-size: .86rem; color: var(--text-3); }
.era .timeline { padding-top: 16px; padding-bottom: 8px; }
.era[hidden] { display: none; }

/* An entry filtered out by search or the type buttons. */
.node[hidden] { display: none; }

.badge-essential {
    margin-left: 9px; vertical-align: 2px;
    font-size: .64rem; font-weight: 750; text-transform: uppercase; letter-spacing: .09em;
    color: var(--on-accent); background: var(--accent);
    padding: 2px 7px; border-radius: var(--radius-sm); white-space: nowrap;
}

/* Controls that cannot work without scripting are not shown without it. The
 * search box needs to filter live and "unwatched" depends on what this browser
 * knows; neither can be a link. Everything that CAN be a real URL already is. */
[data-type-filter], .search-field, [data-stats] { display: none; }
.has-js [data-type-filter] { display: flex; }
.has-js .search-field { display: block; }
.has-js [data-stats] { display: grid; }

/* --- the staged arrival --------------------------------------------------
 *
 * Landing on a franchise shows the three cards and nothing else. Pick one and
 * the switches appear; press the button and the timeline arrives.
 *
 * Three rules hold this together.
 *
 * The class is on <html> in the markup, not added by a script, so there is no
 * flash of the whole page before it collapses. A <noscript> block in the head
 * undoes it all, because with no scripting there is nothing to advance the
 * stages and the reader should simply get the page.
 *
 * And every stage is `display: none`, not opacity or height. A section that is
 * merely transparent is still in the tab order and still read out, so somebody
 * navigating by keyboard or by screen reader would be walking through a hundred
 * entries the page is pretending not to have shown them yet.
 */

.is-staged .switches,
.is-staged .timeline-head,
.is-staged .era { display: none; }

.is-staged .path-cards { margin-bottom: 0; }

/* Stage 2: a path is chosen, the switches are in. */
.is-staged.stage-filters .switches { display: block; }
.is-staged.stage-filters .stage-go { display: inline-block; }

/* Stage 3: everything. */
.is-staged.stage-timeline .timeline-head,
.is-staged.stage-timeline .era { display: block; }
.is-staged.stage-timeline .stage-go { display: none; }

/* Sits at the END of the switch panel, not above it: the button is the last
 * thing you do, so it belongs after the choices it acts on rather than in
 * front of them, where it invites a click before anything has been set. */
.stage-go {
    display: none; margin: 16px 0 2px;
    font: inherit; font-size: .92rem; font-weight: 700; cursor: pointer;
    padding: 11px 20px; border-radius: var(--radius-sm);
    background: var(--accent); color: var(--on-accent); border: 1px solid var(--accent);
}
.stage-go:hover { background: var(--accent-hi); border-color: var(--accent-hi); }
.stage-hint {
    margin: 10px 0 0; font-size: .86rem; color: var(--text-3);
}

/* --- materialising ------------------------------------------------------
 *
 * The list arrives over about three and a half seconds, each entry resolving
 * from the bottom upward behind a scan line.
 *
 * The look is Marvel's, which is a specific thing: a hot white core with a
 * coloured bloom around it, not a coloured line. Every glow below is layered
 * the same way — white in the middle, the franchise colour spreading out — and
 * that is what separates a laser from a highlighter.
 *
 * The reveal is a clip-path inset animated on the TOP edge, from 100% down to
 * zero, which grows the visible band upward from the bottom of the card. Doing
 * it with opacity alone reads as a fade; doing it this way reads as arriving.
 *
 * The stagger stays under a second across the whole list, with the lower
 * entries a fraction ahead. A strict bottom-to-top sweep over a hundred and
 * three entries would mean somebody at the top of the page watches an empty
 * column for three seconds while it happens out of sight below them.
 */

@media (prefers-reduced-motion: no-preference) {
    .stage-beaming .node {
        animation: materialise 3s cubic-bezier(.16, .8, .3, 1) both;
        animation-delay: var(--beam, 0ms);
    }

    /* The scan line: a thin bar that travels up through each card as the card
     * resolves behind it. Clipped to the card, so it reads as passing through
     * the thing rather than floating over the page. */
    .stage-beaming .card { position: relative; overflow: hidden; }
    .stage-beaming .card::after {
        content: ""; position: absolute; left: -10%; right: -10%; bottom: 0; height: 2px;
        pointer-events: none; z-index: 3;
        background: linear-gradient(90deg,
            transparent,
            color-mix(in srgb, var(--brand) 60%, white) 18%,
            #fff 50%,
            color-mix(in srgb, var(--brand) 60%, white) 82%,
            transparent);
        box-shadow:
            0 0 6px 1px #fff,
            0 0 18px 3px var(--brand),
            0 0 44px 8px color-mix(in srgb, var(--brand) 55%, transparent);
        animation: scanline 3s cubic-bezier(.16, .8, .3, 1) both;
        animation-delay: var(--beam, 0ms);
    }

    /* The edge lights up as the card lands, then lets go. */
    .stage-beaming .node-entry .card,
    .stage-beaming .node-beat .card {
        animation: edge-charge 3s ease-out both;
        animation-delay: var(--beam, 0ms);
    }

    .stage-beaming .node::before { animation: link-in .8s ease-out both;
                                   animation-delay: calc(var(--beam, 0ms) + 1.9s); }
    .stage-beaming .era-head {
        animation: beam-head .7s ease-out both;
        animation-delay: var(--beam, 0ms);
    }

    /* The column running up the rail. A white core inside a wide bloom, which
     * is the difference between a beam and a stripe. */
    .stage-beaming .timeline::after {
        content: ""; position: absolute; left: 15px; top: 0; bottom: 0; width: 3px;
        pointer-events: none;
        background: linear-gradient(to bottom,
            transparent, #fff 40%, #fff 60%, transparent);
        box-shadow:
            0 0 8px 2px var(--brand),
            0 0 26px 6px color-mix(in srgb, var(--brand) 70%, transparent),
            0 0 60px 14px color-mix(in srgb, var(--brand) 35%, transparent);
        animation: beam-rise 2.8s ease-in-out both;
        animation-delay: var(--beam-start, 0ms);
    }
    @media (max-width: 640px) { .stage-beaming .timeline::after { display: none; } }
}

@keyframes materialise {
    from {
        opacity: 0;
        clip-path: inset(100% 0 0 0);
        filter: brightness(2.8) saturate(1.6) blur(3px);
        transform: translateY(10px);
    }
    35% { opacity: .55; filter: brightness(2) saturate(1.4) blur(1.5px); }
    70% { opacity: 1; clip-path: inset(0 0 0 0); filter: brightness(1.2) blur(0); }
    to  { opacity: 1; clip-path: inset(0 0 0 0); filter: none; transform: none; }
}

/* Travels from the bottom of the card to just past the top, then goes out. */
@keyframes scanline {
    from { transform: translateY(0); opacity: 0; }
    8%   { opacity: 1; }
    70%  { transform: translateY(calc(-100% + 2px)); opacity: 1; }
    to   { transform: translateY(calc(-100% + 2px)); opacity: 0; }
}

@keyframes edge-charge {
    from { box-shadow: 0 0 0 1px #fff, 0 0 22px -2px var(--brand); }
    70%  { box-shadow: 0 0 0 1px color-mix(in srgb, var(--brand) 70%, transparent),
                       0 0 14px -4px var(--brand); }
    to   { box-shadow: var(--shadow); }
}

@keyframes link-in {
    from { opacity: 0; transform: scaleY(0); transform-origin: top;
           box-shadow: 0 0 8px 2px var(--brand); }
    to   { opacity: .45; transform: scaleY(1); transform-origin: top; box-shadow: none; }
}
@keyframes beam-head {
    from { opacity: 0; transform: translateY(8px); filter: brightness(2.4) saturate(1.5); }
    to   { opacity: 1; transform: none; filter: none; }
}
@keyframes beam-rise {
    from { opacity: 0; transform: scaleY(0); transform-origin: bottom; }
    22%  { opacity: 1; }
    to   { opacity: 0; transform: scaleY(1); transform-origin: bottom; }
}
}
}
