/* ══════════════════════════════════════════════════════════════════════════
   CAMPAIGN COGITATOR — terminal theme
   This :root block is the single source of truth for colour. Change it HERE
   ONLY — nothing else in this file hard-codes a hex value.
   ══════════════════════════════════════════════════════════════════════════ */

:root {
    --bg:          #05070a;   /* near-black, faint blue-green cast */
    --bg-panel:    #0a0f0c;   /* panel / table background          */
    --bg-hover:    #10170f;   /* row hover                         */
    --green:       #33ff66;   /* primary phosphor                  */
    --green-dim:   #1a8039;   /* secondary text, labels            */
    --green-faint: #0f4020;   /* BORDERS AND RULES ONLY — too dark for text */
    --amber:       #ffb000;   /* rank 1, highlights, warnings      */
    --red:         #ff2e2e;   /* losses, alerts                    */
    --bone:        #d8d2c0;   /* parchment tone, long-form text    */

    --mono: 'JetBrains Mono', 'IBM Plex Mono', 'Fira Code', ui-monospace,
            SFMono-Regular, Menlo, Consolas, monospace;

    --glow: 0 0 6px currentColor;
}

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

html { color-scheme: dark; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--green);
    font-family: var(--mono);
    font-size: 14px;
    line-height: 1.55;
    letter-spacing: 0.01em;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

/* Faint green wash + horizon glow, so the black isn't flat */
body::before {
    content: "";
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 0;
    background:
        radial-gradient(ellipse 120% 60% at 50% 0%, rgba(51, 255, 102, 0.06), transparent 70%),
        radial-gradient(ellipse 100% 50% at 50% 100%, rgba(51, 255, 102, 0.03), transparent 70%);
}

/* ── CRT overlays ────────────────────────────────────────────────────────── */

.crt-scanlines {
    position: fixed;
    inset: 0;
    z-index: 9000;
    pointer-events: none;
    background: repeating-linear-gradient(
        to bottom,
        rgba(0, 0, 0, 0)    0px,
        rgba(0, 0, 0, 0)    2px,
        rgba(0, 0, 0, 0.22) 3px,
        rgba(0, 0, 0, 0.22) 4px
    );
    animation: crt-drift 8s linear infinite;
}

.crt-vignette {
    position: fixed;
    inset: 0;
    z-index: 8999;
    pointer-events: none;
    background: radial-gradient(ellipse at center, transparent 52%, rgba(0, 0, 0, 0.62) 100%);
}

@keyframes crt-drift { from { background-position: 0 0; } to { background-position: 0 4px; } }
@keyframes blink     { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

/* ── Feed columns ────────────────────────────────────────────────────────── */

/* Two feeds crawling down the screen edges: sector dispatches on the left,
   litanies on the right. Deliberately dim — they are texture, not content,
   and must never compete with the leaderboard. */
.feed {
    position: fixed;
    top: 0;
    width: 208px;
    height: 100vh;
    overflow: hidden;
    pointer-events: none;
    z-index: 0;
    background: var(--bg-panel);

    /* Legible, but clearly behind the content in the hierarchy. */
    opacity: 0.7;
}

/* Border on the inner edge only, so each reads as a screen bezel. */
.feed--left  { left: 0;  border-right: 1px solid var(--green-faint); }
.feed--right { right: 0; border-left: 1px solid var(--green-faint); }

.feed__label {
    padding: 0.9rem 1.1rem 0.7rem;
    border-bottom: 1px solid var(--green-faint);
    color: var(--green);
    font-size: 0.58rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    white-space: nowrap;
    overflow: hidden;
}

.feed__viewport {
    position: absolute;
    top: 2.6rem;
    bottom: 0;
    left: 0;
    right: 0;
    overflow: hidden;

    /* Fade both ends so lines emerge and dissolve rather than being clipped. */
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 12%, #000 88%, transparent);
    mask-image: linear-gradient(to bottom, transparent, #000 12%, #000 88%, transparent);
}

.feed__track {
    /* Travels exactly half its height — one full copy of the shuffled list —
       so the restart lands on an identical frame and the seam is invisible. */
    animation: feed-scroll 200s linear infinite;
}

/* Different pace per side, so the two columns never march in lockstep. */
.feed--left .feed__track { animation-duration: 260s; }

@keyframes feed-scroll {
    from { transform: translateY(0); }
    to   { transform: translateY(-50%); }
}

.feed__line {
    margin: 0 0 1.5rem;
    padding: 0 1.1rem;
    color: var(--green-dim);
    font-size: 0.6rem;
    line-height: 1.7;
    letter-spacing: 0.09em;
    text-transform: uppercase;
}

/* NOTE: never pad .feed__track — it scrolls exactly -50% of its own height to
   land on an identical frame. Anything added inside it shifts that boundary
   and the loop develops a visible jump. Pad .feed__line instead. */

/* Content is 900px, each feed 208px. Drop the left feed first, then both,
   rather than letting either crowd the leaderboard. */
@media (max-width: 1359px) {
    .feed--left { display: none; }
}

@media (max-width: 1139px) {
    .feed { display: none; }
}

/* ── Layout shell ────────────────────────────────────────────────────────── */

.term {
    position: relative;
    z-index: 1;
    /* Narrow enough that the leaderboard's names and numbers stay visually
       related, and long prose keeps a comfortable measure. */
    max-width: 900px;
    margin: 0 auto;
    padding-block: 2rem 3rem;
    padding-inline: 16px;
}

.term__head { border-bottom: 1px solid var(--green-faint); padding-bottom: 1rem; }

.term__stamp {
    color: var(--green-dim);
    font-size: 0.7rem;
    letter-spacing: 0.22em;
    text-transform: uppercase;
}

.term__title {
    margin: 0.4rem 0 0;
    font-size: clamp(1.35rem, 5vw, 2.2rem);
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    text-shadow: var(--glow);

    /* Typewriter. width:max-content makes 100% mean "the width of the text",
       so the animation reveals character by character. steps() is what makes
       it tick rather than slide. */
    width: max-content;
    max-width: 100%;
    overflow: hidden;
    white-space: nowrap;
    animation: type 0.55s steps(18, end) both;
}

@keyframes type {
    from { width: 0; }
    to   { width: 100%; }
}

/* The header and body print in sequence, like a terminal filling a screen.
   Kept under ~0.8s end to end — this runs on every visit, and people checking
   the standings should not have to sit through a title sequence. */
@keyframes reveal {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: none; }
}

/* The header lives in MainLayout, which survives navigation — so these run
   once, on first load only. */
.term__sub   { animation: reveal 0.25s ease-out 0.22s both; }
.term__strip { animation: reveal 0.25s ease-out 0.30s both; }
.nav         { animation: reveal 0.25s ease-out 0.38s both; }

/* The body is replaced on every tab change, so these re-run each time. Keep
   them short — a delay that's charming once is tiresome on the twentieth
   navigation. Whole sequence lands inside 0.6s. */
.term__body .prompt { animation: reveal 0.22s ease-out both; }
.term__body .prompt:nth-of-type(1) { animation-delay: 0.04s; }
.term__body .prompt:nth-of-type(2) { animation-delay: 0.11s; }
.term__body .prompt:nth-of-type(3) { animation-delay: 0.18s; }

/* Uniform delay rather than a per-panel stagger: the match log renders one
   panel per game night, so a stagger would leave late panels crawling in. */
.term__body .panel { animation: panel-in 0.42s ease-out 0.24s both; }

/* Settles with a CRT flicker, as though the screen is redrawing. */
@keyframes panel-in {
    0%   { opacity: 0; transform: translateY(6px); }
    35%  { opacity: 0.9; }
    45%  { opacity: 0.35; }
    60%  { opacity: 1; }
    100% { opacity: 1; transform: none; }
}

.term__sub {
    margin: 0.25rem 0 0;
    color: var(--bone);
    opacity: 0.75;
    font-size: 0.8rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}

/* Status strip: AUTH / STATUS / date-stamp */
.term__strip {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.5rem;
    margin-top: 1rem;
    font-size: 0.68rem;
    letter-spacing: 0.16em;
    color: var(--green-dim);
    text-transform: uppercase;
}

.term__strip b { color: var(--green); font-weight: 400; }

/* ── Navigation ──────────────────────────────────────────────────────────── */

.nav {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.5rem;
    margin-top: 1.25rem;
}

.nav a {
    color: var(--green-dim);
    text-decoration: none;
    font-size: 0.7rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    transition: color 0.12s linear;
}

.nav a:hover { color: var(--green); }

/* Blazor's NavLink adds .active to the matching route. */
.nav a.active { color: var(--amber); text-shadow: var(--glow); }

.term__body { margin-top: 2.5rem; }

.term__foot {
    margin-top: 3.5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--green-faint);
    color: var(--green-dim);
    font-size: 0.7rem;
    letter-spacing: 0.14em;
}

.term__foot p { margin: 0.35rem 0; }

.thought {
    color: var(--bone);
    opacity: 0.8;
    font-style: italic;
    letter-spacing: 0.04em;
    text-transform: none;
    font-size: 0.78rem;
}

/* ── Prompt lines ────────────────────────────────────────────────────────── */

.prompt {
    margin: 0 0 0.35rem;
    color: var(--green-dim);
    font-size: 0.75rem;
    letter-spacing: 0.12em;
    /* Values interpolated from the data files (season name, dates) arrive in
       whatever case they were typed. Uppercase here keeps the header uniform. */
    text-transform: uppercase;
}

.prompt b { color: var(--green); font-weight: 400; }

.cursor {
    display: inline-block;
    margin-left: 0.15em;
    color: var(--green);
    animation: blink 1.1s steps(1) infinite;
}

/* ── Panel (bordered box with an inset title, like a fieldset) ───────────── */

.panel {
    position: relative;
    margin-top: 2.25rem;
    border: 1px solid var(--green-faint);
    background: var(--bg-panel);
    padding: 1.75rem 1rem 1rem;
}

.panel__title {
    position: absolute;
    top: 0;
    left: 1rem;
    transform: translateY(-50%);
    background: var(--bg);
    padding: 0 0.65rem;
    color: var(--green);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.22em;
    text-transform: uppercase;
}

/* Corner ticks, for the hand-drawn ASCII feel */
.panel::before, .panel::after {
    content: "";
    position: absolute;
    width: 8px;
    height: 8px;
    border-color: var(--green);
    pointer-events: none;
}
.panel::before { top: -1px; right: -1px; border-top: 1px solid; border-right: 1px solid; }
.panel::after  { bottom: -1px; left: -1px; border-bottom: 1px solid; border-left: 1px solid; }

.notice {
    margin: 0 0 1rem;
    color: var(--amber);
    font-size: 0.7rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

/* ── Fault / empty states ────────────────────────────────────────────────── */

.alert { color: var(--red); }

.panel--alert { border-color: rgba(255, 46, 46, 0.45); }
.panel--alert::before, .panel--alert::after { border-color: var(--red); }
.panel__title--alert { color: var(--red); }
.notice--alert { color: var(--red); }

.fault {
    margin: 0 0 1rem;
    padding: 0.75rem 0.9rem;
    background: #1a0505;
    border-left: 2px solid var(--red);
    color: var(--red);
    font-size: 0.78rem;
    word-break: break-word;
}

.hint {
    margin: 0;
    color: var(--bone);
    opacity: 0.7;
    font-size: 0.78rem;
}

.faults {
    margin: 0;
    padding: 0;
    list-style: none;
}

.faults li {
    padding: 0.5rem 0.9rem;
    margin-bottom: 0.4rem;
    background: #1a0505;
    border-left: 2px solid var(--red);
    color: var(--red);
    font-size: 0.76rem;
    word-break: break-word;
}

code {
    color: var(--amber);
    font-family: var(--mono);
    font-size: 0.92em;
}

.empty {
    margin: 1.5rem 0;
    color: var(--green-dim);
    font-size: 0.8rem;
    letter-spacing: 0.14em;
}

/* ── Standings table ─────────────────────────────────────────────────────── */

.table-scroll { overflow-x: auto; }

.standings {
    width: 100%;
    min-width: 360px;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}

.standings thead th {
    padding: 0 0.55rem 0.5rem;
    border-bottom: 1px solid var(--green-faint);
    color: var(--green-dim);
    font-size: 0.64rem;
    font-weight: 400;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    text-align: left;
    white-space: nowrap;
}

.standings td {
    padding: 0.6rem 0.55rem;
    border-bottom: 1px dashed rgba(26, 128, 57, 0.28);
    white-space: nowrap;
}

/* Must out-specify `.standings thead th`, which left-aligns by default */
.standings thead th.num { text-align: right; }

/* Anything carrying a native tooltip signals it on hover. */
.standings thead th[title] { cursor: help; }

.standings tbody tr { transition: background-color 0.12s linear; }
.standings tbody tr:hover { background: var(--bg-hover); }
.standings tbody tr:last-child td { border-bottom: none; }

.num { text-align: right; }

.rank { color: var(--green-dim); width: 1%; }

.commander { color: var(--green); font-weight: 700; letter-spacing: 0.08em; }

.faction {
    display: block;
    color: var(--bone);
    opacity: 0.55;
    font-size: 0.68rem;
    font-weight: 400;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.w { color: var(--green); }
.d { color: var(--bone); opacity: 0.7; }
.l { color: var(--red); opacity: 0.85; }

/* A zero carries no news. Without this, nil losses render in alarm-red, which
   reads as a problem when it is the best cell in the table. Out-specifies the
   .w/.d/.l colours above. */
.standings td.is-zero {
    color: rgba(216, 210, 192, 0.3);
    opacity: 1;
}

/* Leader / wooden spoon */
.is-first .commander,
.is-first .rank { color: var(--amber); text-shadow: var(--glow); }

.tag {
    display: inline-block;
    margin-left: 0.5rem;
    padding: 0.05rem 0.35rem;
    border: 1px solid currentColor;
    font-size: 0.58rem;
    font-weight: 400;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    vertical-align: middle;
    white-space: nowrap;

    /* Signals there's an explanation behind it. The tooltip itself is a native
       title attribute — a styled one would be clipped by .table-scroll, which
       establishes a scroll container and therefore cuts off anything absolutely
       positioned outside it. */
    cursor: help;
}

/* Status reflects the last engagement, not league position. Escalating runs
   get the stronger wording and the stronger colour. */
.tag--ascendant { color: var(--amber); text-shadow: var(--glow); }   /* 3+ wins  */
.tag--victory   { color: var(--green); }                             /* last: won */
.tag--stalemate { color: var(--bone); opacity: 0.65; }               /* last: drew */
.tag--defeat    { color: var(--red); opacity: 0.6; }                 /* last: lost */
.tag--penance   { color: var(--red); opacity: 0.9; }                 /* 3+ losses */
.tag--none      { color: var(--green-dim); }                         /* no games  */

/* ── Match log ───────────────────────────────────────────────────────────── */

.engagement {
    padding: 0.9rem 0;
    border-bottom: 1px dashed rgba(26, 128, 57, 0.28);
}

.engagement:last-child { border-bottom: none; padding-bottom: 0.2rem; }

.engagement__head {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem 1rem;
    color: var(--green-dim);
    font-size: 0.64rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}

.engagement__id { color: var(--green); }

.engagement__combatants {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem 0.75rem;
    margin-top: 0.5rem;
}

.combatant {
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.combatant.is-victor   { color: var(--amber); text-shadow: var(--glow); }
.combatant.is-defeated { color: var(--red); opacity: 0.7; }
.combatant.is-drawn    { color: var(--bone); opacity: 0.8; }

.engagement__vs {
    color: var(--green-dim);
    font-size: 0.7rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}

.engagement__verdict {
    margin: 0.4rem 0 0;
    color: var(--green-dim);
    font-size: 0.68rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

.engagement__verdict b { color: var(--amber); font-weight: 400; }
.engagement__verdict.is-draw { color: var(--bone); opacity: 0.65; }

/* Which territory the result moved — ties the log to the Theatre of War. */
.engagement__territory {
    margin: 0.25rem 0 0;
    color: var(--green-dim);
    font-size: 0.68rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}
.engagement__territory b { color: var(--bone); font-weight: 400; }
.engagement__maplink { margin-left: 0.4rem; color: var(--green-dim); text-decoration: none; white-space: nowrap; }
.engagement__maplink:hover, .engagement__maplink:focus-visible { color: var(--green); text-shadow: var(--glow); }

.engagement__playlabel {
    margin: 0.7rem 0 0.15rem;
    color: var(--green-dim);
    font-size: 0.6rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
}

/* The players' own words — quoted, so it reads as testimony. */
.engagement__notes {
    margin: 0.5rem 0 0;
    max-width: 66ch;
    color: var(--bone);
    opacity: 0.55;
    font-size: 0.76rem;
    font-style: italic;
    letter-spacing: 0.02em;
}

/* The same battle as campaign record: upright, not quoted, slightly warmer,
   so it reads as the archive speaking rather than a player. */
.engagement__chronicle {
    margin: 0.5rem 0 0;
    max-width: 66ch;
    padding-left: 0.9rem;
    border-left: 1px solid var(--green-faint);
    color: var(--bone);
    opacity: 0.78;
    font-size: 0.8rem;
    line-height: 1.75;
    letter-spacing: 0.01em;
}

/* ── Commander dossiers ──────────────────────────────────────────────────── */

.dossier__name {
    margin: 0;
    font-size: clamp(1.1rem, 4vw, 1.5rem);
    font-weight: 700;
    letter-spacing: 0.14em;
    color: var(--green);
    text-shadow: var(--glow);
}

.dossier__faction {
    margin: 0.2rem 0 1.25rem;
    color: var(--bone);
    opacity: 0.6;
    font-size: 0.72rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}

/* Label column sized to the longest label, so values line up in a stat block. */
.dossier__specs {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.45rem 1.5rem;
    margin: 0 0 1.5rem;
}

.dossier__specs dt {
    color: var(--green-dim);
    font-size: 0.62rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
}

.dossier__specs dd {
    margin: 0;
    color: var(--green);
    font-size: 0.8rem;
    letter-spacing: 0.04em;
}

.dossier__record {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem 1rem;
    font-size: 0.76rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
}

/* NOTE: the form string is no longer rendered anywhere — CommanderStatus
   derives the status tag from it instead. StandingsCalculator still computes
   it, and that is what feeds the status. Don't delete it from the model. */

.dossier__lorelabel {
    margin: 0 0 0.35rem;
    color: var(--green-dim);
    font-size: 0.6rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
}

.dossier__lore {
    margin: 0;
    max-width: 68ch;
    color: var(--bone);
    opacity: 0.8;
    font-size: 0.82rem;
    line-height: 1.7;
    letter-spacing: 0.01em;
}

.dossier__lore--missing {
    color: var(--amber);
    opacity: 0.7;
    font-style: italic;
}

@media (max-width: 520px) {
    /* Stack labels above values rather than squeezing two columns. */
    .dossier__specs { grid-template-columns: 1fr; gap: 0.1rem; }
    .dossier__specs dd { margin-bottom: 0.6rem; }
}

.dossier__maplink { color: inherit; text-decoration: none; border-bottom: 1px dashed var(--green-faint); }
.dossier__maplink:hover, .dossier__maplink:focus-visible { color: var(--green); border-bottom-color: var(--green); }

/* ── Sector brief (campaign lore) ────────────────────────────────────────── */

.brief__designation {
    margin: 0 0 1.5rem;
    color: var(--green-dim);
    font-size: 0.66rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}

.brief__epigraph {
    margin: 0 0 2rem;
    padding-left: 1rem;
    border-left: 2px solid var(--green-faint);
    max-width: 62ch;
    color: var(--amber);
    opacity: 0.85;
    font-size: 0.88rem;
    font-style: italic;
    line-height: 1.7;
}

.brief__heading {
    margin: 2rem 0 0.8rem;
    color: var(--green);
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.2em;
    text-transform: uppercase;
}

/* Long prose in monospace needs a tight measure or it becomes unreadable. */
.brief__body {
    margin: 0 0 1rem;
    max-width: 66ch;
    color: var(--bone);
    opacity: 0.82;
    font-size: 0.82rem;
    line-height: 1.75;
    letter-spacing: 0.01em;
}

.claimant {
    padding: 1.25rem 0;
    border-bottom: 1px dashed rgba(26, 128, 57, 0.28);
}

.claimant:last-child { border-bottom: none; padding-bottom: 0.2rem; }

.claimant__name {
    margin: 0;
    color: var(--green);
    font-size: 0.95rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    text-shadow: var(--glow);
}

.claimant__meta {
    margin: 0.2rem 0 0.8rem;
    color: var(--bone);
    opacity: 0.55;
    font-size: 0.68rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

.claimant__objective {
    margin: 0 0 0.9rem;
    color: var(--amber);
    opacity: 0.85;
    font-size: 0.72rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

/* The undeclared players — deliberately colder than the named war-parties. */
.claimant--unknown .claimant__name {
    color: var(--green-dim);
    text-shadow: none;
}

.brief__closing {
    margin: 2.5rem 0 0;
    max-width: 66ch;
    color: var(--amber);
    opacity: 0.75;
    font-size: 0.8rem;
    font-style: italic;
    line-height: 1.7;
}

/* ── Theatre of War (the globe) ──────────────────────────────────────────── */

/* wwwroot/js/globe.js builds this SVG and re-projects it every frame. Faction
   tints come from players.json ("colour") and arrive as each tile's `color`,
   so every part of a tile below just uses currentColor. */

.theatre {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 260px;
    gap: 0 1.25rem;
    align-items: start;
}

.globe-host { aspect-ratio: 620 / 470; }
.globe {
    display: block;
    width: 100%;
    height: auto;
    cursor: grab;
    /* Horizontal drags turn the world; vertical ones still scroll the page on a phone. */
    touch-action: pan-y;
    user-select: none;
    -webkit-user-select: none;
}
.globe.is-dragging { cursor: grabbing; }

.globe__body { fill: var(--bg); }
.globe__rim { fill: none; stroke: var(--green-dim); stroke-opacity: 0.6; stroke-width: 1; pointer-events: none; }
.globe__halo { fill: url(#globeHalo); pointer-events: none; }
.globe__shade { pointer-events: none; }
.halo-stop-in   { stop-color: var(--green); stop-opacity: 0.1; }
.halo-stop-out  { stop-color: var(--green); stop-opacity: 0; }
.limb-stop-clear { stop-color: var(--bg); stop-opacity: 0; }
.limb-stop-dark  { stop-color: var(--bg); stop-opacity: 0.85; }
.light-stop-in  { stop-color: var(--bone); stop-opacity: 0.07; }
.light-stop-out { stop-color: var(--bone); stop-opacity: 0; }

.orbit { fill: none; stroke: var(--bone); stroke-opacity: 0.18; stroke-width: 0.8; stroke-dasharray: 2 5; pointer-events: none; }

.cell { cursor: crosshair; }
.cell__fill { fill: currentColor; fill-opacity: 0.2; transition: fill-opacity 0.15s; }
.cell__rim { fill: none; stroke: currentColor; stroke-opacity: 0.6; stroke-width: 1; }
.cell__keep { fill: none; stroke: currentColor; stroke-opacity: 0.8; stroke-width: 1; stroke-dasharray: 3 2; pointer-events: none; }

.cell:hover .cell__fill, .cell.is-active .cell__fill { fill-opacity: 0.42; }
.cell:hover .cell__rim,  .cell.is-active .cell__rim  { stroke-opacity: 1; stroke-width: 1.4; }

/* The latest engagement: its border flickers amber until the next one. */
.cell--recent .cell__rim { stroke: var(--amber); stroke-opacity: 0.9; animation: recent-flicker 2.4s steps(1) infinite; }
@keyframes recent-flicker { 0%, 100% { stroke-opacity: 0.9; } 40% { stroke-opacity: 0.35; } 44% { stroke-opacity: 1; } }

/* No glow filter here — labels move every frame, and a filter per label per
   frame is what makes a phone stutter. A dark halo keeps them legible instead. */
.cell__label {
    fill: currentColor;
    stroke: var(--bg);
    stroke-width: 2.4px;
    stroke-opacity: 0.8;
    paint-order: stroke;
    font-family: var(--mono);
    font-size: 9.5px;
    letter-spacing: 0.06em;
    text-anchor: middle;
    text-transform: uppercase;
}
.globe__labels { pointer-events: none; }

/* Uncharted: dark red with flickering static. Hostile, not merely empty. */
.cell--uncharted { color: var(--red); }
.cell--uncharted .cell__fill { fill: #1a0505; fill-opacity: 0.92; }
.cell--uncharted .cell__rim { stroke-opacity: 0.35; }
.cell--uncharted .cell__keep { stroke-opacity: 0.4; }
.cell__static { fill: url(#hatch); pointer-events: none; }
.cell__static, .cell__label--lost { animation: static-flicker 3.2s steps(1) infinite; }
.hatch-line { stroke: var(--red); stroke-width: 1; stroke-opacity: 0.35; }

@keyframes static-flicker {
    0%, 100% { opacity: 0.55; }
    7%  { opacity: 0.15; }
    9%  { opacity: 0.7; }
    52% { opacity: 0.35; }
    55% { opacity: 0.6; }
}

/* The Counting Moon: bone on a black void, sealed. The only body in no army's
   colour, so it reads as neutral at a glance. It pulses: it is the signal. */
.moon { cursor: crosshair; color: var(--bone); }
.moon__body { fill: var(--bg); stroke: var(--bone); stroke-width: 1.2; }
.moon:hover .moon__body, .moon.is-active .moon__body { fill: var(--bg-hover); stroke-width: 1.8; }
.moon__seal { fill: none; stroke: var(--bone); stroke-opacity: 0.4; stroke-width: 0.8; stroke-dasharray: 1 2.5; }
.moon__core { fill: var(--bone); filter: drop-shadow(0 0 3px currentColor); }
.moon__pulse {
    fill: none; stroke: var(--bone); stroke-opacity: 0.5;
    pointer-events: none;
    transform-origin: 0 0;
    animation: moon-pulse 3.4s ease-out infinite;
}
@keyframes moon-pulse {
    from { transform: scale(0.45); opacity: 0.8; }
    to   { transform: scale(1.5);  opacity: 0; }
}

/* Phone-only summary under the globe; the side panels are below the fold there. */
.globe-card { display: none; }
@media (max-width: 760px) {
    .globe-card {
        display: block;
        margin-top: 0.75rem;
        padding-top: 0.75rem;
        border-top: 1px dashed var(--green-faint);
        font-size: 0.78rem;
    }
}
.globe-card p { margin: 0; }
.globe-card__name { font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; text-shadow: var(--glow); }
.globe-card__meta { margin-top: 0.15rem !important; color: var(--green-dim); font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase; }
.globe-card__desc { margin-top: 0.5rem !important; color: var(--bone); line-height: 1.55; }

/* ── Territory register (the globe as a list) ── */
.register summary {
    cursor: pointer;
    color: var(--green-dim);
    font-size: 0.72rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}
.register summary:hover, .register summary:focus-visible { color: var(--green); }
.register__army {
    margin: 1.25rem 0 0.4rem;
    font-size: 0.8rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.register__army span { margin-left: 0.5rem; color: var(--green-dim); font-weight: 400; font-size: 0.66rem; letter-spacing: 0.14em; }
.register__army--lost { color: var(--red); }
.register__lost { margin: 0; color: var(--bone); opacity: 0.7; font-size: 0.75rem; }
.register__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 0 1rem;
}
.register__item {
    display: block;
    width: 100%;
    padding: 0.3rem 0.4rem;
    border: 0;
    border-bottom: 1px dashed var(--green-faint);
    background: none;
    color: var(--bone);
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.register__item:hover, .register__item:focus-visible, .register__item.is-active { background: var(--bg-hover); outline: none; }
.register__item.is-active .register__name { color: var(--green); }
.register__name { display: block; font-size: 0.76rem; }
.register__meta { display: block; color: var(--green-dim); font-size: 0.62rem; letter-spacing: 0.12em; text-transform: uppercase; }

.readout { font-size: 0.78rem; }
.readout__name {
    margin: 0 0 0.15rem;
    font-size: 0.95rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-shadow: var(--glow);
}
.readout__name--lost { color: var(--red); }
.readout__name--moon { color: var(--bone); }
.readout__meta {
    margin: 0 0 0.8rem;
    color: var(--green-dim);
    font-size: 0.68rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}
.readout__desc { margin: 0 0 0.8rem; color: var(--bone); line-height: 1.6; }
.readout__tag {
    margin: 0 0 0.6rem;
    color: var(--amber);
    font-size: 0.66rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}
.readout__tag--alert { color: var(--red); }
.readout__label {
    margin: 0.4rem 0 0.3rem;
    color: var(--green-dim);
    font-size: 0.66rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}
.readout__history { list-style: none; margin: 0; padding: 0; color: var(--bone); font-size: 0.72rem; }
.readout__history li { padding: 0.2rem 0; border-bottom: 1px dashed var(--green-faint); }
.readout__history b { color: var(--green); font-weight: 400; margin-right: 0.35rem; }

.claim-list { list-style: none; margin: 0; padding: 0; font-size: 0.72rem; }
.claim-list li {
    display: grid;
    grid-template-columns: 12px 1fr auto;
    gap: 0.6rem;
    align-items: center;
    padding: 0.3rem 0;
    border-bottom: 1px dashed var(--green-faint);
}
.claim-list li:last-child { border-bottom: 0; }
.claim-list__swatch {
    width: 12px; height: 12px;
    border: 1px solid currentColor;
    background: color-mix(in srgb, currentColor 20%, transparent);
}
.claim-list__army { color: var(--bone); }
.claim-list__count { color: var(--green-dim); letter-spacing: 0.1em; }
.claim-list__lost { color: var(--red); }
.claim-list__moon { color: var(--bone); }
.claim-list__moon .claim-list__swatch { background: var(--bg); border-radius: 50%; }
.claim-list__lost .claim-list__swatch { background: #1a0505; }
.claim-list__lost .claim-list__army { color: var(--red); }

@media (max-width: 760px) {
    .theatre { grid-template-columns: minmax(0, 1fr); }
}

/* ── Legend ──────────────────────────────────────────────────────────────── */

.legend {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem 1.25rem;
    margin-top: 1rem;
    color: var(--green-dim);
    font-size: 0.64rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

/* ── Boot screen (shown while the WASM runtime downloads) ────────────────── */

/* Full-screen cover. Sits above the page but below the CRT overlays, so the
   scanlines and vignette play over the boot sequence too. */
.boot-overlay {
    position: fixed;
    inset: 0;
    z-index: 8800;
    background: var(--bg);
    overflow-y: auto;
    transition: opacity 0.55s ease;

    /* A tap anywhere skips it — signal that it's interactive. */
    cursor: pointer;
}

.boot-overlay.is-done {
    opacity: 0;
    pointer-events: none;
}

.boot {
    position: relative;
    z-index: 1;
    max-width: 640px;
    margin: 0 auto;
    padding: 5rem 16px;
    color: var(--green);
}

.boot__mark {
    margin: 0 0 2rem;
    color: var(--green-dim);
    font-size: 0.7rem;
    line-height: 1.3;
    letter-spacing: 0.1em;
}

/* Self-test lines: label on the left, verdict on the right, dotted leader
   between them. They print in sequence rather than appearing at once. */
.boot__line {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    margin: 0 0 0.45rem;
    font-size: 0.72rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--green-dim);

    /* Each line arrives ~110ms after the last; see nth-child below. */
    animation: reveal 0.2s ease-out both;
}

/* The dotted leader — a flexible rule that fills whatever space is left. */
.boot__line span {
    flex: 0 1 auto;
}

.boot__line::after {
    content: "";
    flex: 1 1 auto;
    height: 0;
    margin-bottom: 0.25em;
    border-bottom: 1px dotted var(--green-faint);
    order: 1;
}

.boot__line b {
    order: 2;
    color: var(--green);
    font-weight: 400;
    white-space: nowrap;
}

.boot__line b.is-warn { color: var(--amber); }

/* Each check takes a visible moment: the label prints, then the verdict lands
   ~0.35s later, so it reads as the cogitator actually testing something rather
   than a list fading in. Full sequence runs about 3.5 seconds.

   NOTE: the boot screen is replaced the instant the WASM runtime finishes
   downloading, so on a warm cache this gets cut short. That's correct — a cold
   boot should take longer than a warm one. */
.boot__line:nth-child(2) { animation-delay: 0.30s; }
.boot__line:nth-child(3) { animation-delay: 0.92s; }
.boot__line:nth-child(4) { animation-delay: 1.54s; }
.boot__line:nth-child(5) { animation-delay: 2.16s; }
.boot__line:nth-child(6) { animation-delay: 2.78s; }
.boot__line:nth-child(7) { animation-delay: 3.40s; }
.boot__line:nth-child(8) { animation-delay: 4.02s; }

/* The verdict resolves ~0.4s after its label, so each check visibly takes a
   moment rather than arriving complete. */
.boot__line b { animation: reveal 0.18s ease-out both; }

.boot__line:nth-child(2) b { animation-delay: 0.70s; }
.boot__line:nth-child(3) b { animation-delay: 1.32s; }
.boot__line:nth-child(4) b { animation-delay: 1.94s; }
.boot__line:nth-child(5) b { animation-delay: 2.56s; }
.boot__line:nth-child(6) b { animation-delay: 3.18s; }
.boot__line:nth-child(7) b { animation-delay: 3.80s; }
.boot__line:nth-child(8) b { animation-delay: 4.42s; }

/* The mark and the closing lines bracket the sequence. */
.boot__mark { animation: reveal 0.3s ease-out both; }
.boot__bar  { animation: reveal 0.3s ease-out 4.6s both; }
.boot__rite { animation: reveal 0.3s ease-out 4.8s both; }
.boot__warn { animation: reveal 0.3s ease-out 5.0s both; }

.boot__warn {
    margin-top: 0.75rem;
    color: var(--red);
    opacity: 0.55;
    font-size: 0.6rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

/* The held state. Every check has resolved; this is what the screen sits on
   while the runtime finishes downloading, however long that takes. It pulses
   indefinitely rather than running to an end, so there's no dead frame. */
.boot__enter {
    margin-top: 1.5rem;
    color: var(--amber);
    font-size: 0.78rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    text-shadow: var(--glow);
    /* Appears once every check has resolved, then pulses until the overlay is
       dismissed — the sequence never runs out before the app arrives. */
    animation:
        reveal 0.3s ease-out 5.2s both,
        boot-pulse 1.6s ease-in-out 5.5s infinite;
}

@keyframes boot-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.45; }
}

/* Appears early — the people who want it are the ones who aren't watching the
   rest. Deliberately quiet so it doesn't undercut the sequence. */
.boot__skip {
    margin-top: 2rem;
    color: var(--green-dim);
    font-size: 0.6rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    animation: reveal 0.4s ease-out 1.5s both;
}

.boot__bar {
    margin-top: 1.5rem;
    height: 3px;
    background: var(--green-faint);
    overflow: hidden;
}

.boot__bar span {
    display: block;
    width: 35%;
    height: 100%;
    background: var(--green);
    box-shadow: var(--glow);
    animation: boot-sweep 1.4s ease-in-out infinite;
}

@keyframes boot-sweep {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(385%); }
}

.boot__rite {
    margin-top: 2rem;
    color: var(--green-dim);
    font-size: 0.64rem;
    letter-spacing: 0.2em;
}

/* ── Blazor error bar ────────────────────────────────────────────────────── */

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 9500;
    padding: 0.8rem 1.2rem;
    background: #1a0505;
    border-top: 1px solid var(--red);
    color: var(--red);
    font-size: 0.72rem;
    letter-spacing: 0.14em;
}

#blazor-error-ui a, #blazor-error-ui .dismiss {
    color: var(--red);
    cursor: pointer;
    margin-left: 1rem;
    text-decoration: none;
}

/* ── Responsive ──────────────────────────────────────────────────────────── */

/* The status column is the first thing to go when space runs short — the
   record itself matters more than the commentary on it. */
@media (max-width: 700px) {
    .col-status { display: none; }
}

@media (max-width: 520px) {
    /* Remaining columns fit a phone unaided; let the table use full width. */
    .standings { min-width: 0; }
    .term { padding-block: 1.5rem 2.5rem; }
}

/* ── Motion / contrast preferences ───────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
    .crt-scanlines { animation: none; }
    .cursor        { animation: none; }
    .boot__bar span { animation: none; width: 100%; }

    /* Perpetual scrolling in peripheral vision is the exact thing this
       preference exists to stop. Removed outright, not just paused. */
    .feed { display: none; }

    /* Everything arrives at once, fully formed. No typing, no fade-in. */
    .term__title {
        animation: none;
        width: auto;
        white-space: normal;
        overflow: visible;
    }

    .term__sub,
    .term__strip,
    .nav,
    .boot__mark,
    .boot__line,
    .boot__line b,
    .boot__bar,
    .boot__rite,
    .boot__warn,
    .boot__enter,
    .boot__skip,
    .term__body .prompt,
    .term__body .panel { animation: none; }

    /* The globe holds still unless dragged (globe.js checks this too). */
    .moon__pulse { display: none; }
    .cell__static, .cell__label--lost, .cell--recent .cell__rim { animation: none; }

    * { transition: none !important; }
}

@media (prefers-contrast: more) {
    .crt-scanlines, .crt-vignette { display: none; }
    .faction, .d { opacity: 1; }
}
