SpaceCities / style.css
Claude
An in-app AI editor: build an opponent dial by dial
4979046 unverified
Raw History Blame
71.8 kB
/* ===== Stellar Frontier: RTS β€” styles =====
Palette carried over from the original Stellar Frontier's style.css so
the two games read as the same universe. Everything below the :root
block is new β€” the original's layout was built for a turn-based tab/panel
UI and doesn't fit a real-time view. */
:root {
--bg: #05070f;
--bg-grad-1: #0a1028;
--bg-grad-2: #060814;
--panel: #0e1530;
--panel-2: #141d42;
--border: #243162;
--accent: #4fd1ff;
--accent-2: #8a6bff;
--good: #4ade80;
--bad: #f87171;
--warn: #fbbf24;
--text: #dce6ff;
--muted: #8593c4;
--gold: #ffd166;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: radial-gradient(ellipse at top, var(--bg-grad-1), var(--bg-grad-2));
color: var(--text);
font-family: "Segoe UI", system-ui, sans-serif;
overflow: hidden;
}
#app {
display: flex;
flex-direction: column;
height: 100%;
}
/* TWO FIXED ROWS. The topbar carries a lot (resources, clock, chips, volume, mute,
pause/galaxy/save/load/home/help). It used to be ONE wrapping row, so as the resource
readout grew or shrank the control buttons flipped between line 1 and line 2 β€” a jarring
up/down jump. Now it's a column: an INFO row (line 1) that stays a single line (the
resource readout scrolls sideways instead of wrapping), and a CONTROLS row (line 2) that
is always pinned below it, so the buttons never move. */
#topbar {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 6px;
padding: 8px 12px;
background: var(--panel);
border-bottom: 1px solid var(--border);
}
.topbar-row {
display: flex;
align-items: center;
gap: 8px 12px;
min-width: 0; /* let flex children shrink instead of forcing the row wider */
}
/* Line 1 never wraps β€” a long commodity list scrolls within the resource readout, keeping
the row's height constant so the controls below can't jump. Every fixed item keeps its size
and its text on one line; only the resource readout shrinks (and scrolls) to absorb the
squeeze β€” otherwise the title text itself would wrap to several lines and grow the row. */
.topbar-info { flex-wrap: nowrap; }
.topbar-info > * { flex: 0 0 auto; white-space: nowrap; }
.topbar-info > .resources { flex: 1 1 auto; min-width: 0; }
/* Line 2 keeps the buttons together; on a very narrow window they may wrap among themselves,
but they always live on their own row, never mixed in with the resources. */
.topbar-controls { flex-wrap: wrap; }
#topbar .title {
font-weight: 700;
letter-spacing: 0.08em;
color: var(--accent);
}
.resources {
display: flex;
gap: 14px;
font-size: 0.85rem;
color: var(--muted);
flex: 1 1 auto;
min-width: 0;
/* Stay on ONE line and scroll sideways when the commodity list is long, so the info row's
height never changes and the control row below it can't be shoved up/down. */
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
}
.resources::-webkit-scrollbar { height: 4px; }
.resources::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.resources > * { flex: 0 0 auto; white-space: nowrap; } /* each readout keeps its width; the row scrolls, not wraps */
.resources span { color: var(--text); }
/* Supply readout warns amber at the cap, flashes red for the ~800ms after a
supply-blocked production attempt (see main.js's supplyBlockedUntil). */
.resources .supply.at-cap { color: var(--warn); }
.resources .supply.blocked { color: var(--bad); }
/* Odyssey universal-credit balance β€” galaxy-wide, so it reads gold like the clock. */
.resources .credits { color: var(--gold); font-variant-numeric: tabular-nums; }
/* Odyssey industrial-Power gauge β€” reads like supply, warns amber when factories
out-draw the Reactors and production throttles. */
.resources .power { font-variant-numeric: tabular-nums; }
.resources .power.at-cap { color: var(--warn); }
/* Neighbour stance chip β€” green when friendly, amber neutral, red hostile. */
.resources .relation { text-transform: capitalize; }
.resources .relation.friendly { color: var(--good, #4ade80); }
.resources .relation.neutral { color: var(--warn); }
.resources .relation.hostile { color: var(--bad); }
/* Commodity flow ledger (docs/improvement-proposals.md "Commodity flow ledger"): a resource span
reads red only once it's BOTH net-negative and something live actually depends on it (hud.js
liveConsumers) β€” a stockpile drifting down with nothing standing on it yet stays the ordinary
color. Hover any resource for the +X/min Β· -X/min tooltip that drives this. */
.resources span.deficit { color: var(--bad); }
/* Galaxy toasts β€” brief top-center alerts about worlds you're not on. #galaxyToast is a
STACK (flex column) holding up to 3 toasts, so a routine "Researched…" no longer evicts a
one-shot clickable "colony has fallen β€” click to retake" (overlays.js showGalaxyToast). */
.galaxy-toast-stack {
position: absolute; top: 64px; left: 50%; transform: translateX(-50%);
display: flex; flex-direction: column; align-items: center; gap: 6px;
z-index: 35; pointer-events: none; /* above .starmap-overlay (30) so "colony under attack/lost" alerts stay visible with the starmap open */
}
.galaxy-toast-stack.hidden { display: none; }
.galaxy-toast {
padding: 8px 16px; border-radius: 8px; font-size: 0.9rem; font-weight: 600;
background: #12203a; border: 1px solid #2b3a5e; color: var(--text);
box-shadow: 0 4px 16px rgba(0,0,0,0.5); pointer-events: none;
transition: opacity 0.3s;
}
.galaxy-toast.warn { border-color: var(--warn); color: var(--warn); }
.galaxy-toast.bad { border-color: var(--bad); color: var(--bad); }
.galaxy-toast.good { border-color: var(--good, #4ade80); color: var(--good, #4ade80); }
/* An actionable alert (e.g. "colony under attack β€” click to defend"): the base
toast is pointer-events:none, so re-enable clicks and cue it as interactive. */
.galaxy-toast.clickable { pointer-events: auto; cursor: pointer; }
.galaxy-toast.clickable:hover { background: #1a2c4d; box-shadow: 0 4px 20px rgba(0,0,0,0.6); }
/* Control-group chips ("1:8 2:3") β€” top-left over the field, so bound groups are visible
and (on touch, which has no number row) tappable to recall. Rebuilt by hud.js each tick. */
.group-chips { position: absolute; top: 10px; left: 10px; display: flex; gap: 6px; z-index: 15; pointer-events: none; }
.group-chips.hidden { display: none; }
.group-chip {
pointer-events: auto; font-family: inherit; cursor: pointer;
padding: 3px 9px; border-radius: 6px; font-size: 0.8rem; font-weight: 700;
background: #12203a; border: 1px solid #2b3a5e; color: var(--text);
}
.group-chip:hover { background: #1a2c4d; border-color: var(--accent, #4fd1ff); }
/* Transient UI hint (touch "why is this greyed" feedback) β€” bottom-center, its own channel
so it never competes with the galaxy-alert stack. */
.ui-hint {
position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%);
padding: 8px 16px; border-radius: 8px; font-size: 0.9rem; font-weight: 600;
background: #12203a; border: 1px solid var(--warn); color: var(--warn);
box-shadow: 0 4px 16px rgba(0,0,0,0.5); z-index: 22; pointer-events: none; max-width: 80%;
}
.ui-hint.hidden { display: none; }
/* Starmap overlay (starmap.js): a full-screen galaxy map with world nodes on a
ring, coloured by your relationship to each. */
.starmap-overlay {
position: absolute; inset: 0; z-index: 30;
background: rgba(3, 6, 15, 0.92);
display: flex; flex-direction: column; align-items: center;
}
.starmap-overlay.hidden { display: none; }
.chip-btn.hidden { display: none; }
.starmap-head { text-align: center; margin-top: 24px; }
.starmap-head h2 { margin: 0 0 4px; color: var(--accent); letter-spacing: 0.08em; }
.starmap-head p { margin: 0; color: var(--muted); font-size: 0.9rem; }
.starmap-field { position: relative; flex: 1; width: min(760px, 92%); align-self: center; }
.starmap-world {
position: absolute; transform: translate(-50%, -50%);
display: flex; flex-direction: column; align-items: center; gap: 2px;
min-width: 96px; padding: 8px 10px; cursor: pointer;
background: #0e1830; border: 2px solid #2b3a5e; border-radius: 10px;
color: var(--text); box-shadow: 0 2px 10px rgba(0,0,0,0.4);
}
.starmap-world:hover { border-color: var(--accent); }
.starmap-world .sm-ico { font-size: 20px; line-height: 1; } /* faction emblem (data.js FACTIONS) */
.starmap-world .sm-name { font-weight: 600; font-size: 0.9rem; }
.starmap-world .sm-sub { font-size: 0.72rem; color: var(--muted); }
/* Industry/Tech badge β€” the world's economic character at a glance. */
.starmap-world .sm-stats { font-size: 0.68rem; color: var(--muted); opacity: 0.85; font-variant-numeric: tabular-nums; margin-top: 1px; }
/* Deposit icons + yield, plus the world's rule-modifier label when it has one β€” the same
geography read the skirmish select cards already give freely (setup.js's mod-note). A world's
full modifier label (e.g. Glacius's "Frozen ground: all units 10% slower; ice fields flank a
central lane") can run to 6+ wrapped lines at this width, which used to make that one card far
taller than its neighbours on the ring layout below β€” tall enough to overlap and obscure them.
Clamping to 2 lines bounds every card to roughly the same height regardless of label length; the
full, untruncated text is still one hover away via the element's title (starmap.js). */
.starmap-world .sm-deps {
font-size: 0.66rem; color: var(--muted); opacity: 0.8; margin-top: 2px; max-width: 160px; text-align: center;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.starmap-world.seat { border-color: var(--accent); box-shadow: 0 0 14px rgba(79,209,255,0.5); }
.starmap-world.seat .sm-sub { color: var(--accent); }
.starmap-world.colony { border-color: var(--good, #4ade80); }
.starmap-world.colony .sm-sub { color: var(--good, #4ade80); }
.starmap-world.contested { border-color: var(--bad); opacity: 0.85; }
.starmap-world.contested .sm-sub { color: var(--bad); }
/* Conquered β€” its neighbour's capital is razed (Domination win progress). */
.starmap-world.pacified { border-color: var(--gold, #fbbf24); }
.starmap-world.pacified .sm-sub { color: var(--gold, #fbbf24); }
/* Live colony alert badges (game.colonyAlerts, starmap.js renderStarmap/alertBadge): a world
currently on fire β€” or one still waiting to be retaken β€” reads as urgent at a glance instead of
looking identical to a healthy colony. Declared after the status classes above so it wins the
border/glow cascade on an equally-specific selector (e.g. a colony under attack): the live alert
is the more urgent fact right now. */
.starmap-world.alert-attack { border-color: var(--bad); box-shadow: 0 0 14px rgba(255,80,80,0.45); animation: sm-alert-pulse 1.6s ease-in-out infinite; }
.starmap-world.alert-attack .sm-sub { color: var(--bad); font-weight: 600; }
.starmap-world.alert-fallen { border-color: var(--bad); opacity: 0.9; }
.starmap-world.alert-fallen .sm-sub { color: var(--bad); font-weight: 600; }
@keyframes sm-alert-pulse {
0%, 100% { box-shadow: 0 0 14px rgba(255,80,80,0.45); }
50% { box-shadow: 0 0 22px rgba(255,80,80,0.8); }
}
/* Garrison line β€” CC count + combat-unit supply on a world you currently hold (starmap.js
garrisonOf), styled like the industry/tech badge above it. */
.starmap-world .sm-garrison { font-size: 0.68rem; color: var(--muted); opacity: 0.9; margin-top: 1px; font-variant-numeric: tabular-nums; }
/* Colony standing orders + Freight Lanes summary (starmap.js renderColonyOrders/
renderLaneOverlay) β€” reuse the market-head/market-row/market-btn idiom from the selection
panel (hudSelection.js) so a starmap-side list needs no styling of its own beyond width/spacing. */
.starmap-side { width: min(760px, 92%); align-self: center; margin: 4px 0; }
.starmap-side .market-head { margin: 10px 0 4px; }
.starmap-foot { color: var(--muted); font-size: 0.8rem; margin-bottom: 18px; }
/* Surrender β€” the only way out of the play-forever Odyssey. Muted by default (it's a rare,
deliberate act), turning red once armed for the confirm click. */
.starmap-surrender {
margin-bottom: 20px; padding: 7px 16px; border-radius: 8px; cursor: pointer;
background: transparent; color: var(--muted); border: 1px solid #2b3a5e;
font-size: 0.82rem; font-weight: 600; transition: all 0.15s;
}
.starmap-surrender:hover { color: var(--text); border-color: #3a4c74; }
.starmap-surrender.armed { color: var(--bad); border-color: var(--bad); background: rgba(255,80,80,0.08); }
/* Tech & Industry Chart overlay (techChart.js): the unlock ladder laid out as tier COLUMNS β€”
every building, unit gate and tech/doctrine node, prereq depth left-to-right, coloured by
whether it's built/researched, buildable right now, or still locked. Same full-screen
overlay idiom as .starmap-overlay just above (own element, topbar button + hotkey + Esc). */
.techchart-overlay {
position: absolute; inset: 0; z-index: 30;
background: rgba(3, 6, 15, 0.94);
display: flex; flex-direction: column; align-items: center;
padding: 20px 0 0;
}
.techchart-overlay.hidden { display: none; }
.techchart-head { text-align: center; padding: 0 20px; flex: 0 0 auto; }
.techchart-head h2 { margin: 0 0 4px; color: var(--accent); letter-spacing: 0.08em; }
.techchart-head p { margin: 0; color: var(--muted); font-size: 0.85rem; max-width: 720px; }
/* The tier columns scroll sideways as a whole (a deep Odyssey chart easily outgrows the
viewport width); each column scrolls independently on the vertical axis, so a crowded
Tier 0 doesn't stretch every other column's cards down to match its height. */
.techchart-body {
flex: 1 1 auto; width: 100%; margin-top: 14px;
display: flex; gap: 14px; align-items: flex-start;
overflow-x: auto; overflow-y: hidden;
padding: 0 20px 16px;
}
.techchart-col {
flex: 0 0 200px; max-height: 100%;
display: flex; flex-direction: column; gap: 8px;
overflow-y: auto; padding-right: 2px;
}
.techchart-col-head {
flex: 0 0 auto; color: var(--muted); font-size: 0.72rem; font-weight: 700;
letter-spacing: 0.06em; text-transform: uppercase; text-align: center;
border-bottom: 1px solid var(--border); padding-bottom: 6px; margin-bottom: 2px;
}
/* One node card. A plain <div> (built, or any non-building node) reads the same as a
<button> (a clickable building) β€” the state colour carries the meaning, not the tag. */
.techchart-node {
display: flex; align-items: flex-start; gap: 8px; text-align: left;
width: 100%; padding: 7px 8px; font: inherit; color: var(--text);
background: var(--panel-2); border: 1px solid var(--border); border-left: 3px solid var(--border);
border-radius: 6px; cursor: default;
}
button.techchart-node { cursor: pointer; }
button.techchart-node:hover { border-color: var(--accent); }
.techchart-node .btn-icon { margin-top: 1px; flex-shrink: 0; }
/* The text column beside the icon β€” name/cost/recipe/detail stack vertically here, so the
card reads top-to-bottom next to a single fixed-size icon tile instead of the icon repeating
per line. */
.techchart-node-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.techchart-node.built { border-left-color: var(--good, #4ade80); opacity: 0.82; }
.techchart-node.afford { border-left-color: var(--accent); background: rgba(79, 209, 255, 0.07); }
.techchart-node.locked { border-left-color: var(--bad); opacity: 0.72; }
.techchart-node .techchart-name { display: block; font-weight: 600; font-size: 0.82rem; }
.techchart-node .techchart-cost { display: block; font-size: 0.7rem; color: var(--gold); margin-top: 1px; }
.techchart-node .techchart-recipe { display: block; font-size: 0.68rem; color: var(--accent-2); margin-top: 2px; }
.techchart-node .techchart-detail { display: block; font-size: 0.68rem; color: var(--muted); margin-top: 3px; line-height: 1.35; }
.techchart-node.locked .techchart-detail { color: var(--bad); opacity: 0.9; }
.techchart-node.afford .techchart-detail { color: var(--accent); }
.techchart-foot { color: var(--muted); font-size: 0.8rem; margin: 4px 0; flex: 0 0 auto; }
.techchart-manual-link {
color: var(--accent-2); font-size: 0.8rem; text-decoration: none;
margin-bottom: 16px; flex: 0 0 auto;
}
.techchart-manual-link:hover { color: var(--accent); text-decoration: underline; }
/* Odyssey market panel (hudSelection.js renderMarket): a header plus a compact
commodity/price + trend + Sell/Sell x4/Sell All/Buy row per tradeable good. */
.market-head { margin: 8px 0 4px; font-size: 0.8rem; color: var(--gold); letter-spacing: 0.03em; }
.market-row { display: flex; align-items: center; gap: 6px; margin: 3px 0; }
.market-com { flex: 1; font-size: 0.82rem; color: var(--text); text-transform: capitalize; }
.market-btn {
padding: 3px 8px; font-size: 0.76rem; cursor: pointer;
background: #16203a; color: var(--text);
border: 1px solid #2b3a5e; border-radius: 5px;
}
.market-btn:hover { background: #1e2c4d; }
.market-btn.disabled { opacity: 0.4; cursor: default; }
/* The glut/pressure trend glyph (marketTrend, hudSelection.js): a small pre-buttons readout so a
deep, slow glut (worth exporting around) reads apart from an ordinary, fast-recovering dip. */
.market-trend { font-size: 0.72rem; white-space: nowrap; color: var(--muted); }
.clock {
font-variant-numeric: tabular-nums;
color: var(--gold);
}
/* The final-5-minutes countdown (hud.js renderHUD) β€” warn-colored so the clock itself signals
"the score tiebreak is close" before a player even reads the score bar beside it. */
.clock.endgame { color: var(--warn); font-weight: 700; }
/* The compact two-sided score readout (playerScore(state,"player"/"ai")) that appears alongside
the countdown β€” otherwise the score driving the tiebreak is completely invisible in-game. */
.score-bar {
font-variant-numeric: tabular-nums;
font-size: 0.8rem;
color: var(--muted);
border: 1px solid var(--border);
border-radius: 6px;
padding: 3px 8px;
white-space: nowrap;
}
.score-bar.hidden { display: none; }
.mute-btn {
background: none;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 1rem;
line-height: 1;
padding: 6px 8px;
cursor: pointer;
}
.mute-btn:hover { border-color: var(--accent); }
.mute-btn[aria-pressed="true"] { color: var(--muted); }
.volume {
width: 74px;
accent-color: var(--accent);
cursor: pointer;
}
/* Respect a viewer's reduced-motion preference: hold the under-attack banner
steady instead of pulsing it (the world/minimap pings are also de-animated in
render.js's drawEffects). */
@media (prefers-reduced-motion: reduce) {
.under-attack { animation: none; }
}
#main {
flex: 1;
display: flex;
min-height: 0;
}
#view {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg);
padding: 12px;
}
#field {
width: 100%;
height: 100%;
border: 1px solid var(--border);
border-radius: 6px;
cursor: crosshair;
}
.minimap {
position: absolute;
left: 24px;
bottom: 24px;
width: 200px;
height: 125px;
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
background: var(--bg);
}
.under-attack {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
padding: 8px 18px;
background: rgba(248, 113, 113, 0.15);
border: 1px solid var(--bad);
border-radius: 6px;
color: var(--bad);
font-weight: 700;
letter-spacing: 0.04em;
animation: alert-pulse 0.9s ease-in-out infinite;
cursor: pointer; /* clickable β€” jumps the camera to the attack (main.js) */
}
.under-attack:hover { background: rgba(248, 113, 113, 0.28); }
.under-attack.hidden { display: none; }
/* In a scenario, the scenario bar (top:12px, z-index:5) paints over the banner's
default top:16px slot β€” and pirates/escorts are ai-owned, so the banner fires there
constantly. Drop it below the bar (scoped so it never collides with the Odyssey
galaxy-toast at top:64px, which scenarios never show) and lift it above the bar. */
body.scenario .under-attack { top: 64px; z-index: 6; }
/* Persistent Antimatter Gate charge strip (hud.js renderHUD, docs/improvement-proposals.md lines
745-753): a slim always-visible readout of the player's wonder so the finale's clock stays
on-screen without keeping the Gate selected. Top-right so it never collides with the centered
under-attack banner / galaxy toast stack above it. Color warms with progress (base accent ->
warm -> hot) and flips to a dedicated stalled look (bad-red, pulsing like the under-attack
banner) when charge hasn't advanced since the last tick β€” the silent-stall case the proposal
calls out (starved feed goods or throttled Power). */
.gate-chip {
position: absolute;
top: 16px;
right: 16px;
padding: 8px 18px;
background: rgba(79, 209, 255, 0.12);
border: 1px solid var(--accent);
border-radius: 6px;
color: var(--accent);
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer; /* clickable β€” jumps the camera to the Gate (hud.js) */
}
.gate-chip:hover { background: rgba(79, 209, 255, 0.24); }
.gate-chip.hidden { display: none; }
.gate-chip.warm { border-color: var(--warn); color: var(--warn); background: rgba(251, 191, 36, 0.12); }
.gate-chip.hot { border-color: var(--bad); color: var(--bad); background: rgba(248, 113, 113, 0.12); }
/* Observer Mode (observer.js/observerPanel.js): a free-look, read-only spectator over the
Odyssey galaxy. The banner is a full-width strip at the very top edge β€” deliberately louder
than the under-attack pill / gate-chip below it, since "you can't issue orders right now" is
the one state a player must never be confused about. The panel sits opposite the minimap
(bottom-left) so the two never collide. */
.observer-banner {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 6px 16px;
background: rgba(79, 209, 255, 0.16);
border-bottom: 1px solid var(--accent);
color: var(--accent);
font-weight: 700;
letter-spacing: 0.02em;
text-align: center;
z-index: 7;
pointer-events: none; /* navigation happens via the galaxy map / keys, never by clicking the banner itself */
}
.observer-banner.hidden { display: none; }
/* The spectate bar for a WATCHED AI-vs-AI match (observerPanel.js, docs/competitions-and-elo.md
Phase 5): sits directly under the observer banner, and unlike that banner it IS interactive β€”
speed, the exhibition disclosure, and the way out all live here. Centered rather than
full-width so it reads as a control strip, not a second alarm. */
.spectate-bar {
position: absolute;
top: 34px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
max-width: calc(100% - 32px);
padding: 6px 12px;
background: rgba(5, 7, 15, 0.88);
border: 1px solid var(--accent);
border-radius: 8px;
font-size: 12px;
z-index: 8;
}
.spectate-bar.hidden { display: none; }
.spectate-vs { color: var(--accent); font-weight: 700; }
.spectate-speeds { display: flex; align-items: center; gap: 4px; }
.spectate-speed-label { color: var(--muted); text-transform: uppercase; font-size: 10.5px; letter-spacing: 0.04em; }
.spectate-speed-btn {
min-width: 32px;
padding: 3px 8px;
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 11.5px;
font-weight: 700;
cursor: pointer;
}
.spectate-speed-btn:hover { border-color: var(--accent); }
.spectate-speed-btn.active { background: rgba(79, 209, 255, 0.22); border-color: var(--accent); color: var(--accent); }
/* Amber, matching the mode's own .comp-note caveat hue: this is the one line that says a watched
match isn't rated, and it must not read as decoration. */
.spectate-note { color: var(--gold); font-size: 11.5px; }
.spectate-leave-btn {
padding: 3px 10px;
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 11.5px;
cursor: pointer;
}
.spectate-leave-btn:hover { border-color: var(--accent); color: var(--accent); }
/* One block per SEAT in the watched-match stats panel β€” the entrant's name above its own rows. */
.observer-seat-name {
margin-top: 6px;
color: var(--accent);
font-weight: 700;
font-size: 12.5px;
border-top: 1px solid var(--border);
padding-top: 5px;
}
.observer-rows > .observer-seat-name:first-child { margin-top: 0; border-top: none; padding-top: 0; }
.observer-panel {
position: absolute;
right: 16px;
bottom: 24px;
width: 320px;
max-height: 60%;
overflow-y: auto;
padding: 10px 14px;
background: var(--panel);
border: 1px solid var(--accent);
border-radius: 8px;
font-size: 12.5px;
line-height: 1.5;
z-index: 6;
}
.observer-panel.hidden { display: none; }
.observer-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.observer-head h3 { margin: 0; font-size: 14px; flex: 1; }
.observer-ico { font-size: 16px; }
.observer-archetype {
padding: 2px 8px;
border-radius: 999px;
background: var(--panel-2);
color: var(--accent);
font-size: 11px;
font-weight: 700;
}
/* Minimize toggle (observerPanel.js): the stats rows below are the tall part that can cover
whatever's actually happening on the observed world β€” collapsing them down to just the head
(world name + archetype) leaves the map visible while still marking which world is being
watched. State lives as .minimized on #observerPanel itself (see observerPanel.js), so it
survives that element's own innerHTML rebuild every render. */
.observer-min-btn {
flex: 0 0 auto;
width: 20px;
height: 20px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--accent);
font-size: 11px;
line-height: 1;
cursor: pointer;
}
.observer-min-btn:hover { border-color: var(--accent); }
.observer-panel.minimized .observer-rows { display: none; }
.observer-rows { display: flex; flex-direction: column; gap: 4px; }
.observer-row { display: flex; flex-direction: column; }
.observer-label { color: var(--accent); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em; }
.gate-chip.stalled {
border-color: var(--bad);
color: var(--bad);
background: rgba(248, 113, 113, 0.18);
animation: alert-pulse 1.4s ease-in-out infinite;
}
/* Scenarios never show this chip (no Odyssey, no wonder), so no scenario-bar collision rule is
needed here the way body.scenario .under-attack above needs one. */
/* Manual pause (P key): a centered banner over the frozen frame. Help and the Home-confirm
modal carry their own overlays, so ONLY the manual pause shows this. pointer-events:none
so it never intercepts a click (the camera still pans while paused). */
body.paused::after {
content: "⏸ PAUSED";
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 40;
padding: 12px 28px;
background: rgba(5, 7, 15, 0.9);
border: 1px solid var(--warn);
border-radius: 8px;
color: var(--warn);
font-weight: 700;
font-size: 1.1rem;
letter-spacing: 0.08em;
pointer-events: none;
}
@keyframes alert-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.55; }
}
#selectionPanel {
width: 260px;
flex-shrink: 0;
background: var(--panel);
border-left: 1px solid var(--border);
padding: 12px;
overflow-y: auto;
}
.hint {
color: var(--muted);
font-size: 0.85rem;
}
.sel-row {
font-size: 0.85rem;
padding: 4px 0;
border-bottom: 1px solid var(--border);
}
/* A coloured status line under a selected factory (hud.js factoryStatus): green
running, amber throttled, red stalled/starved β€” the "why isn't it producing" cue. */
.sel-note { font-size: 0.82rem; padding: 3px 0 5px; }
.sel-note.good { color: var(--good, #4ade80); }
.sel-note.warn { color: var(--warn); }
.sel-note.bad { color: var(--bad); }
.sel-note.paused { color: var(--muted); }
/* Aggregated type row as a button (hud.js) β€” click "3Γ— Bastion" to sub-select that
type. Strips the default button chrome, keeps the .sel-row look, adds a hover cue. */
.type-row {
display: block; width: 100%; text-align: left;
background: none; border: none; border-bottom: 1px solid var(--border);
color: var(--text); font-family: inherit; cursor: pointer;
}
.type-row:hover { color: var(--accent); background: rgba(255, 255, 255, 0.03); }
/* Section header in the Odyssey build menu (Economy / Military / …) β€” a quiet
divider so the ~18-building list is scannable instead of a flat wall. */
.sel-group {
font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.07em;
color: var(--muted); margin: 9px 0 2px; padding-top: 5px;
border-top: 1px solid var(--border);
}
.sel-group:first-child { border-top: none; padding-top: 0; margin-top: 4px; }
/* Collapsible variant of .sel-group β€” an actual <button>, not a static divider (the Build
submenu toggle: a generalist Worker's full building list, or a mixed selection's union of
categories, can otherwise run long). Keeps the same quiet divider look (including .sel-group's
own border-top, restored here since a <button> needs its default border reset), just clickable. */
.sel-collapsible {
display: block; width: 100%; text-align: left;
background: none; border: none; border-top: 1px solid var(--border);
font-family: inherit; cursor: pointer;
}
.sel-collapsible:first-child { border-top: none; }
.sel-collapsible:hover { color: var(--accent); }
.queue-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: var(--muted);
}
.queue-cancel {
flex-shrink: 0;
background: none;
border: 1px solid var(--border);
border-radius: 3px;
color: var(--bad);
cursor: pointer;
font-size: 0.8rem;
line-height: 1;
padding: 1px 7px;
}
.queue-cancel:hover { border-color: var(--bad); }
.btn {
display: block;
width: 100%;
margin-top: 10px;
padding: 8px 10px;
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.82rem;
cursor: pointer;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
/* Formation shape / leader-position pickers (hudSelection.js) β€” small toggle buttons in a row,
not full-width .btn blocks, so all 4 shapes (or both leader positions) sit on one line. */
.formation-row { display: flex; gap: 6px; margin-top: 6px; }
.formation-btn { flex: 1 1 0; margin-top: 0; padding: 6px 4px; font-size: 0.78rem; text-align: center; }
.formation-btn.active { border-color: var(--accent); background: var(--panel-2); color: var(--accent); }
/* A build/produce button carrying a sprite icon: the icon sits left of the label,
both vertically centered. The icon is the actual unit/building art (render.js
spriteIcon), on a faint inset tile so it reads on the panel background. */
.btn.has-icon { display: flex; align-items: center; gap: 8px; text-align: left; }
.btn-icon {
flex: 0 0 auto; width: 26px; height: 26px;
background: rgba(255, 255, 255, 0.05); border-radius: 4px;
image-rendering: auto;
}
/* An emoji icon (research / doctrine β€” reused from the data icons) centered in the same tile. */
.btn-emoji { display: inline-flex; align-items: center; justify-content: center; font-size: 15px; line-height: 1; }
.btn-label { flex: 1 1 auto; min-width: 0; }
/* Unaffordable build/produce/research buttons: dimmed and unhoverable-looking,
but still clickable so the click can play the "denied" cue (see main.js). */
.btn.disabled { opacity: 0.45; cursor: not-allowed; }
.btn.disabled:hover { border-color: var(--border); color: var(--text); }
/* Home / return-to-menu confirm modal (saveload.js goHome). A centered card over a
dimmed backdrop; reuses .btn for its actions but lays them out inline. */
.home-confirm {
position: fixed; inset: 0; z-index: 40;
display: flex; align-items: center; justify-content: center;
background: rgba(5, 7, 15, 0.82); padding: 20px;
}
.home-card {
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
padding: 22px 26px; max-width: 420px; text-align: center;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
}
.home-card h2 { margin: 0 0 8px; font-size: 1.15rem; color: var(--text); }
.home-card p { margin: 0 0 18px; font-size: 0.9rem; color: var(--muted); }
.home-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.home-actions .btn { width: auto; margin: 0; padding: 10px 16px; }
.btn.primary { border-color: var(--accent); color: var(--accent); }
.btn.ghost { background: none; color: var(--muted); }
.btn.primary:disabled { opacity: 0.4; cursor: not-allowed; border-color: var(--border); color: var(--muted); }
/* Landing-site picker (landingPicker.js) β€” a blind minimap pick shown before a jump lands on a
world with no player Spaceport standing on it. Same overlay/card idiom as .home-confirm, just
wide enough to hold the chart canvas. */
.landing-pick-confirm {
position: fixed; inset: 0; z-index: 40;
display: flex; align-items: center; justify-content: center;
background: rgba(5, 7, 15, 0.82); padding: 20px;
}
.landing-pick-card {
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
padding: 22px 26px; max-width: 540px; text-align: center;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
}
.landing-pick-card h2 { margin: 0 0 8px; font-size: 1.15rem; color: var(--text); }
.landing-pick-card p { margin: 0 0 14px; font-size: 0.9rem; color: var(--muted); }
.landing-pick-canvas-wrap { display: flex; justify-content: center; margin-bottom: 12px; }
.landing-pick-canvas {
max-width: 100%; height: auto; border: 1px solid var(--border); border-radius: 8px;
cursor: crosshair;
}
.landing-pick-hint { font-size: 0.85rem; }
/* Controls reference shown in the empty selection panel. */
.legend { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 10px; }
.legend-row { display: flex; gap: 8px; font-size: 0.74rem; color: var(--muted); padding: 2px 0; }
.legend-key { flex: 0 0 88px; color: var(--accent); }
.overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(5, 7, 15, 0.85);
font-size: 1.4rem;
font-weight: 700;
color: var(--gold);
text-align: center;
padding: 20px;
}
.overlay.hidden { display: none; }
.map-select {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 20px;
/* The setup panel plus nine world cards outgrow the viewport, so align to
the top and let the whole screen scroll β€” centering would clip the title
and bottom cards on shorter windows. */
overflow-y: auto;
padding: 24px 16px;
background: radial-gradient(ellipse at top, var(--bg-grad-1), var(--bg-grad-2));
z-index: 10;
}
.map-select.hidden { display: none; }
.map-select h2 {
color: var(--accent);
letter-spacing: 0.04em;
}
.map-select .cards {
display: flex;
gap: 16px;
flex-wrap: wrap;
justify-content: center;
max-width: 1000px;
}
.map-card {
width: 240px;
text-align: left;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
color: var(--text);
cursor: pointer;
}
.map-card:hover { border-color: var(--accent); }
.map-card .name { font-weight: 700; color: var(--accent); display: block; }
.map-card .tag { font-size: 0.78rem; color: var(--gold); display: block; margin: 4px 0 8px; }
.map-card .desc { font-size: 0.82rem; color: var(--muted); }
/* Who you're fighting, and how the world tilts the fight β€” see main.js's
renderMapSelect. The doctrine line reuses the accent-2 hue; the modifier
line warns amber like every other "this changes the rules" cue. */
.map-card .ai-note { display: block; margin-top: 8px; font-size: 0.78rem; color: var(--accent-2); }
.map-card .mod-note { display: block; margin-top: 4px; font-size: 0.78rem; color: var(--warn); }
/* Odyssey's start-world picker: the same card, just narrower β€” 11 worlds + Random need to fit
without the long desc paragraph (setup.js's renderMapSelect Odyssey branch omits it). */
.map-card.map-card-compact { width: 168px; padding: 12px 14px; }
.map-card.map-card-compact .sm-stats { display: block; margin-top: 6px; font-size: 0.72rem; color: var(--muted); }
/* Pick your side of an asymmetric matchup (Oort, Nimbus): a small pill INSIDE the card, not a
nested button (invalid β€” a button can't contain another interactive control) β€” see setup.js. */
.map-card-swap {
display: block; margin-top: 10px; padding: 5px 10px; border-radius: 6px;
background: var(--bg); border: 1px solid var(--border); color: var(--muted);
font-size: 0.74rem; text-align: center; cursor: pointer;
}
.map-card-swap:hover { border-color: var(--accent-2); color: var(--text); }
.map-card-swap.active { border-color: var(--accent); color: var(--accent); background: var(--panel-2); }
/* ---- splash-screen game setup (AI speed / map size / resources) ---- */
.map-select h3.cards-heading {
color: var(--muted);
font-weight: 600;
font-size: 0.95rem;
margin: 0;
letter-spacing: 0.02em;
}
.setup {
width: min(720px, 92vw);
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px 20px;
display: flex;
flex-direction: column;
gap: 14px;
}
.setup-row {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.setup-label {
flex: 0 0 84px;
color: var(--accent);
font-weight: 600;
font-size: 0.9rem;
}
.setup-value {
color: var(--gold);
font-size: 0.85rem;
min-width: 140px;
}
.setup-hint {
margin: -6px 0 2px;
font-size: 0.76rem;
color: var(--muted);
}
.apm-slider {
flex: 1 1 240px;
accent-color: var(--accent);
cursor: pointer;
}
.opt-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
flex: 1 1 auto;
}
/* The mode toggle is a direct child of the .map-select flex COLUMN, so flex-grow:1
makes it (and its stretched buttons) balloon vertically to fill spare space β€” huge
when a mode like Odyssey shows no card grid below. Only the nested setup-row groups
should grow (horizontally, to fill their row); the mode toggle must not. */
.map-select > .opt-group { flex: 0 0 auto; }
.opt-btn {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 7px 12px;
color: var(--text);
cursor: pointer;
text-align: center;
min-width: 96px;
}
.opt-btn:hover { border-color: var(--accent-2); }
.opt-btn.active { border-color: var(--accent); background: var(--panel-2); }
.opt-btn .opt-label { display: block; font-weight: 600; font-size: 0.86rem; }
.opt-btn .opt-note { display: block; font-size: 0.7rem; color: var(--muted); margin-top: 2px; }
/* ---------- Tier-1 UX: seed chip, help, objectives ---------- */
.seed-chip {
font-size: 0.7rem;
color: var(--muted);
border: 1px solid var(--border);
border-radius: 6px;
padding: 3px 8px;
cursor: pointer;
white-space: nowrap;
}
.seed-chip:hover { border-color: var(--accent-2); color: var(--text); }
.seed-chip.copied { border-color: var(--accent); color: var(--accent); }
.seed-chip.hidden { display: none; }
/* App version chip (topbar) + the menu version line. */
.app-version { font-size: 0.66rem; color: var(--muted); opacity: 0.75; letter-spacing: 0.04em; white-space: nowrap; }
.setup-version { margin: 0 0 10px; color: var(--muted); font-size: 0.78rem; letter-spacing: 0.04em; }
/* Auto-update banner (update.js): a slim strip at the very top of the app. */
.update-banner {
display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
padding: 8px 16px; background: #13233f; border-bottom: 1px solid var(--accent);
color: var(--text); font-size: 0.85rem; z-index: 60;
}
.update-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.update-notes { color: var(--muted); font-weight: 400; }
.update-compat { font-size: 0.8rem; color: var(--muted); }
.update-compat.ok { color: var(--good, #4ade80); }
.update-compat.bad { color: var(--warn); }
.update-actions { display: flex; gap: 8px; flex-shrink: 0; }
.update-btn {
padding: 6px 14px; border-radius: 7px; cursor: pointer; font-size: 0.82rem; font-weight: 600;
background: transparent; color: var(--muted); border: 1px solid var(--border);
}
.update-btn:hover { color: var(--text); border-color: var(--accent-2); }
.update-btn.primary { background: var(--accent); color: #05070f; border-color: var(--accent); }
.faction-chip {
font-size: 0.7rem;
color: var(--accent);
border: 1px solid var(--border);
border-radius: 6px;
padding: 3px 8px;
white-space: nowrap;
cursor: default;
}
.faction-chip.hidden { display: none; }
.help-btn {
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 6px;
width: 30px; height: 30px;
color: var(--text);
cursor: pointer;
font-weight: 700;
}
.help-btn:hover { border-color: var(--accent); }
.objectives {
position: absolute;
top: 48px; left: 50%;
transform: translateX(-50%);
width: min(720px, 92%);
background: rgba(14, 21, 48, 0.94);
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
border-radius: 8px;
padding: 10px 40px 10px 14px;
z-index: 4;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.4);
}
.objectives.hidden { display: none; }
.obj-goal { display: block; font-weight: 700; color: var(--text); font-size: 0.86rem; }
/* The reactive opening checklist (overlays.js renderObjectives/updateObjectives): one row per
predicate, checking off β€” with a brief flash β€” as the match state satisfies it. */
.obj-list { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; }
.obj-row { display: flex; align-items: baseline; gap: 8px; font-size: 0.78rem; color: var(--muted); line-height: 1.4; border-radius: 4px; }
.obj-check { flex: 0 0 14px; color: var(--good, #4ade80); font-weight: 700; }
.obj-row.done { color: var(--text); text-decoration: line-through; text-decoration-color: var(--border); }
.obj-row.flash { animation: obj-flash 900ms ease-out; }
@keyframes obj-flash {
0% { background: rgba(74, 222, 128, 0.35); }
100% { background: transparent; }
}
.obj-close {
position: absolute; top: 6px; right: 10px;
background: transparent; border: none;
color: var(--muted); cursor: pointer;
font-size: 1.2rem; line-height: 1;
}
.obj-close:hover { color: var(--text); }
.help-overlay { z-index: 50; cursor: pointer; }
.help-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 24px;
max-width: 540px;
width: 90%;
cursor: default;
box-shadow: 0 12px 44px rgba(0, 0, 0, 0.55);
}
.help-card h2 { margin: 0 0 12px; font-size: 1.1rem; color: var(--accent); }
.help-row {
display: flex; gap: 12px;
font-size: 0.82rem; font-weight: 400;
color: var(--text);
padding: 5px 0;
border-top: 1px solid rgba(36, 49, 98, 0.5);
text-align: left;
}
.help-key { flex: 0 0 155px; color: var(--accent); font-weight: 600; }
.help-dismiss { margin: 14px 0 0; font-size: 0.74rem; color: var(--muted); font-weight: 400; }
.help-manual { margin: 6px 0 0; font-size: 0.78rem; }
.help-manual a { color: var(--accent-2); text-decoration: none; cursor: pointer; }
.help-manual a:hover { color: var(--accent); text-decoration: underline; }
.seed-input {
flex: 0 0 130px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 10px;
color: var(--text);
font: inherit;
}
.seed-input:focus { outline: none; border-color: var(--accent); }
.gameover-seed { font-size: 0.8rem; font-weight: 400; color: var(--muted); margin-top: 10px; }
/* The competition block on the game-over screen (overlays.js renderCompetitionResult, Phase 4):
what a just-played gauntlet match did to the ladder, and what's next. Boxed off from the
victory/defeat copy above it because it's a different KIND of statement β€” the match's own verdict
versus the run's bookkeeping β€” while staying on the same screen, which is the whole point (a
player wants both at once). */
.gameover-comp {
margin-top: 18px;
padding: 14px 16px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--panel);
max-width: 520px;
text-align: left;
}
.gameover-comp-title { font-size: 0.78rem; color: var(--accent); text-transform: uppercase; letter-spacing: 0.04em; }
.gameover-comp-outcome { font-size: 1.05rem; font-weight: 700; color: var(--text); margin-top: 6px; }
.gameover-comp-rating { font-size: 1.15rem; font-weight: 700; color: var(--gold); margin-top: 6px; }
.gameover-comp-standing { font-size: 0.85rem; color: var(--muted); margin-top: 6px; }
.gameover-comp-error { font-size: 0.82rem; color: var(--bad); margin-top: 8px; }
/* The D4 seat-asymmetry disclosure: quiet, but genuinely legible β€” never a tooltip, and never
small enough to read as fine print nobody is meant to read. */
.gameover-comp-note {
font-size: 0.74rem;
line-height: 1.45;
color: var(--muted);
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
.gameover-comp-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
.gameover-comp-actions .btn { width: auto; margin: 0; padding: 9px 16px; font-size: 0.85rem; }
/* ---- Competition mode (competition.js): Quick Duel / Roster / Standings β€” reuses
.setup/.opt-group/.opt-btn/.seed-input's own visual language throughout; these rules are only
what's genuinely new: the tab row, the two-entrant layout, the roster picker/table, the confirm-
delete modal, the world multi-select, the progress bar, and the results/standings tables. ---- */
.comp-screen {
width: min(760px, 94vw);
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px 20px;
display: flex;
flex-direction: column;
gap: 14px;
text-align: left;
}
/* Deliberately NOT a full-width .btn like Run Duel/Cancel β€” a small corner link back out of the
whole screen, not a primary action of whichever sub-view (config/progress/results) is showing. */
.comp-back-btn {
align-self: flex-start;
width: auto;
margin-top: 0;
padding: 6px 12px;
font-size: 0.76rem;
color: var(--muted);
}
.comp-back-btn:hover { color: var(--text); border-color: var(--accent-2); }
/* Quick Duel / Roster / Standings β€” one coherent mode, three sub-screens (docs/
competitions-and-elo.md Phase 2). Same opt-btn-style pill row as .opt-group elsewhere, kept as
its own class rather than reusing .opt-group directly since these buttons are plain toggles (no
sub-label/note row) rather than optionGroup()'s own {label, note} shape. */
.comp-tabs { display: flex; gap: 8px; flex-wrap: wrap; }
.comp-tab-btn {
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 7px 14px;
color: var(--muted);
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
}
.comp-tab-btn:hover { border-color: var(--accent-2); color: var(--text); }
.comp-tab-btn.active { border-color: var(--accent); color: var(--accent); background: var(--panel); }
.comp-intro { max-width: 640px; margin: 0; }
.comp-entrants { display: flex; gap: 14px; flex-wrap: wrap; }
.comp-entrant {
flex: 1 1 260px;
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
}
.comp-entrant-heading { margin: 0 0 8px; color: var(--accent); font-size: 0.95rem; }
.comp-name-input {
display: block;
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 10px;
margin-bottom: 10px;
color: var(--text);
font: inherit;
}
.comp-name-input:focus { outline: none; border-color: var(--accent); }
.comp-substrategy-label { display: block; margin: 2px 0 6px; }
/* The entrant card's "From Roster" pick β€” a native <select> (keyboard/screen-reader friendly for
free, no custom listbox to build) styled to match .comp-name-input's own input chrome. */
.comp-roster-select {
display: block;
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 10px;
color: var(--text);
font: inherit;
}
.comp-roster-select:focus { outline: none; border-color: var(--accent); }
.comp-worlds { margin-top: 4px; }
.comp-seeds-input {
width: 80px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 10px;
color: var(--text);
font: inherit;
}
.comp-seeds-input:focus { outline: none; border-color: var(--accent); }
.comp-error { color: var(--bad); font-size: 0.82rem; margin: -6px 0 0; }
.comp-progress { display: flex; flex-direction: column; gap: 10px; }
.comp-progress-bar {
height: 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
}
.comp-progress-fill { height: 100%; background: var(--accent); transition: width 150ms ease-out; }
/* The results view's status line β€” .comp-note alone is the warn hue (a genuine caveat, e.g. "the
ladder couldn't be updated"), same "this changes what you'd otherwise assume" cue as .map-card
.mod-note/.update-compat.bad. .comp-note-good (Phase 2: the ordinary, successful case now that a
duel really does update a persistent ladder) swaps to the good hue, same idiom as .sel-note.good/
.update-compat.ok/.galaxy-toast.good elsewhere in this file. */
.comp-note {
margin: 0;
padding: 8px 10px;
font-size: 0.78rem;
color: var(--warn);
background: rgba(251, 191, 36, 0.08);
border: 1px solid rgba(251, 191, 36, 0.35);
border-radius: 6px;
}
.comp-note.comp-note-good {
color: var(--good, #4ade80);
background: rgba(74, 222, 128, 0.08);
border-color: rgba(74, 222, 128, 0.35);
}
.comp-elo-row { display: flex; gap: 14px; flex-wrap: wrap; }
.comp-elo-card {
flex: 1 1 200px;
display: flex;
flex-direction: column;
gap: 2px;
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 16px;
}
.comp-elo-name { font-weight: 600; color: var(--text); font-size: 0.9rem; }
.comp-elo-value { font-size: 1.3rem; color: var(--gold); font-weight: 700; }
.comp-elo-games { font-size: 0.74rem; color: var(--muted); }
/* Results view's action row (Phase 2: now two buttons, Run Another Duel + View Standings) β€” side
by side rather than .btn's own default full-width stack, same pattern as .comp-io-row below. */
.comp-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.comp-actions .btn { flex: 1 1 200px; margin-top: 0; }
.comp-table-wrap { max-width: 100%; overflow-x: auto; border: 1px solid var(--border); border-radius: 8px; }
.comp-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; white-space: nowrap; }
.comp-table th, .comp-table td { padding: 7px 10px; text-align: left; border-bottom: 1px solid var(--border); }
.comp-table th { color: var(--accent); font-weight: 600; background: var(--panel-2); }
.comp-table tbody tr:hover { background: rgba(255, 255, 255, 0.03); }
.comp-table tbody tr:last-child td { border-bottom: none; }
/* Roster table's own Remove action β€” small and quiet (.comp-back-btn's own "not a primary action"
sizing), only reading as dangerous once you actually hover it (an accidental click on a packed
row shouldn't visually shout "delete" at rest). */
.comp-remove-btn {
width: auto;
margin: 0;
padding: 4px 10px;
font-size: 0.72rem;
color: var(--muted);
}
.comp-remove-btn:hover { border-color: var(--bad); color: var(--bad); }
/* Replay (docs/competitions-and-elo.md Phase 5) β€” the per-match action on the Quick Duel results
table and the Standings screen's match history. Same small, quiet geometry as .comp-remove-btn
next to it (a table-cell action, not a primary button), but it warms toward the ACCENT rather
than the danger colour on hover: replaying costs nothing and changes nothing. */
.comp-replay-btn {
width: auto;
margin: 0;
padding: 4px 10px;
font-size: 0.72rem;
color: var(--muted);
white-space: nowrap;
}
.comp-replay-btn:hover { border-color: var(--accent); color: var(--accent); }
/* …and where that button would be for a match that CAN'T be re-run (a live human match, most
often). Deliberately not a disabled button: a disabled control invites clicking to find out why,
while this states the reason on hover and reads as a fact about the row. */
.comp-replay-refused {
font-size: 0.7rem;
color: var(--muted);
opacity: 0.75;
cursor: help;
white-space: nowrap;
}
/* Seasons (Phase 5): the new-season label field β€” .comp-seeds-input's own chrome, just wide enough
for a real name rather than a number. */
.comp-season-input {
flex: 1 1 220px;
min-width: 140px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 10px;
color: var(--text);
font: inherit;
}
.comp-season-input:focus { outline: none; border-color: var(--accent); }
/* The season currently being viewed, marked in the list the same tinted way the human's own
standings row is β€” the table above changed meaning, so which row did that has to be obvious. */
.comp-season-row.is-viewing { background: rgba(56, 189, 248, 0.08); }
/* Standings screen: a provisional entrant (< elo.js's PROVISIONAL_GAMES) gets a small pill next to
its name β€” same gold/warn-adjacent "still settling" read as .comp-elo-games' own inline
"β€” provisional" suffix, just legible on its own in a dense table cell. */
.comp-provisional-badge {
display: inline-block;
margin-left: 6px;
padding: 1px 6px;
font-size: 0.66rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--warn);
background: rgba(251, 191, 36, 0.12);
border: 1px solid rgba(251, 191, 36, 0.35);
border-radius: 10px;
}
/* Standings screen: the human's own row (D4). Same pill geometry as the provisional badge next to
it, in the accent colour the Gauntlet's .comp-human-card already uses for "this one is you" β€”
plus a tinted row, because the mark has to survive a glance down a column of numbers. This is the
one table that ranks a person against the AI field, so which row is theirs cannot be a guess. */
.comp-you-badge {
display: inline-block;
margin-left: 6px;
padding: 1px 6px;
font-size: 0.66rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--accent);
background: rgba(56, 189, 248, 0.12);
border: 1px solid rgba(56, 189, 248, 0.35);
border-radius: 10px;
}
.comp-standings-row.is-you { background: rgba(56, 189, 248, 0.10); }
.comp-standings-row.is-you td { color: var(--text); font-weight: 600; }
/* Roster screen's Export/Import row β€” two full-width .btn's side by side rather than stacked. */
.comp-io-row { display: flex; gap: 10px; }
.comp-io-row .btn { flex: 1 1 0; margin-top: 0; }
/* Roster removal's confirm-before-delete modal β€” same overlay/card idiom as saveload.js's own
.home-confirm/.home-card and landingPicker.js's own .landing-pick-confirm, each with its own
class names rather than literally sharing the first (see landingPicker.js's own comment on why). */
.comp-confirm {
position: fixed; inset: 0; z-index: 40;
display: flex; align-items: center; justify-content: center;
background: rgba(5, 7, 15, 0.82); padding: 20px;
}
.comp-confirm-card {
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
padding: 22px 26px; max-width: 420px; text-align: center;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
}
.comp-confirm-card h3 { margin: 0 0 8px; font-size: 1.1rem; color: var(--text); }
.comp-confirm-card p { margin: 0 0 18px; font-size: 0.88rem; color: var(--muted); }
.comp-confirm-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.comp-confirm-actions .btn { width: auto; margin: 0; padding: 10px 16px; }
.comp-danger-btn { border-color: var(--bad); color: var(--bad); }
.comp-danger-btn:hover { border-color: var(--bad); color: var(--bad); background: rgba(248, 113, 113, 0.12); }
/* ---- Tournament tab (competition.js, docs/competitions-and-elo.md Phase 3): the field-builder
checklist, the up-front cost estimate, and the knockout bracket. Everything else on that screen
(format/difficulty/world pickers, the progress bar, the standings and pairing tables, the results
actions) reuses the classes above unchanged β€” these are only the genuinely new pieces. ---- */
/* Field builder: one checkbox row per roster entry, in a scrolling list so a 30-entry roster
doesn't push the estimate and the Start button off the screen. */
.comp-field-list {
display: flex;
flex-direction: column;
max-height: 260px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel-2);
}
.comp-field-row {
display: flex;
align-items: baseline;
gap: 10px;
padding: 7px 12px;
border-bottom: 1px solid var(--border);
cursor: pointer;
font-size: 0.84rem;
}
.comp-field-row:last-child { border-bottom: none; }
.comp-field-row:hover { background: rgba(255, 255, 255, 0.03); }
.comp-field-row.picked { background: rgba(56, 189, 248, 0.08); }
.comp-field-check { accent-color: var(--accent); cursor: pointer; }
.comp-field-name { color: var(--text); font-weight: 600; }
.comp-field-meta { color: var(--muted); font-size: 0.74rem; }
.comp-field-actions { display: flex; align-items: center; gap: 10px; }
.comp-field-count { color: var(--muted); font-size: 0.76rem; }
/* The pre-run budget line β€” deliberately louder than a .setup-hint (it's the one number that
decides whether to press the button) but not an alarm: the accent hue, same as a table header. */
.comp-estimate {
margin: 0;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel-2);
color: var(--accent);
font-size: 0.9rem;
font-weight: 600;
}
.comp-estimate-detail { display: block; margin-top: 3px; color: var(--muted); font-size: 0.74rem; font-weight: 400; }
.comp-champion {
margin: 0;
color: var(--gold);
font-size: 1.05rem;
font-weight: 700;
}
/* The bracket: one column per round, scrolling horizontally on a narrow screen (a 5-round bracket
is genuinely wide) rather than reflowing, so the left-to-right "who advanced" reading survives. */
.comp-bracket-wrap { max-width: 100%; overflow-x: auto; }
.comp-bracket { display: flex; gap: 14px; align-items: stretch; min-width: min-content; }
.comp-bracket-round { display: flex; flex-direction: column; gap: 10px; justify-content: space-around; min-width: 170px; }
.comp-bracket-title { margin: 0; color: var(--accent); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; }
.comp-bracket-match {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 8px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel-2);
}
.comp-bracket-match.is-bye { border-style: dashed; }
/* The winner has to be unmistakable at a glance β€” bolder, brighter, its own background, plus a
check. Colour alone would leave it ambiguous for anyone who can't see the hue difference. */
.comp-bracket-seat {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 4px 7px;
border-radius: 5px;
color: var(--muted);
font-size: 0.8rem;
}
.comp-bracket-seat.winner { background: rgba(74, 222, 128, 0.14); color: var(--text); font-weight: 700; }
.comp-bracket-seat.bye { color: var(--muted); font-style: italic; }
.comp-bracket-seat-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.comp-bracket-check { color: var(--good, #4ade80); font-weight: 700; }
.comp-bracket-score { align-self: flex-end; color: var(--muted); font-size: 0.7rem; }
/* ---- Gauntlet (docs/competitions-and-elo.md Phase 4): the human against the field. Reuses the
comp- language above wholesale β€” the tab, the field checklist, the estimate line, the elo card,
the tables, the confirm modal β€” so only three genuinely new things live here. ---- */
/* THE SEAT DISCLOSURE (D4). Deliberately NOT a .setup-hint: it sits directly under the human's own
rating and has to read as a qualification OF that number, so it gets its own left rule and a size
that is plainly meant to be read. Not a tooltip, by explicit design. */
.comp-disclosure {
max-width: 640px;
margin: 0;
padding: 8px 0 8px 12px;
border-left: 2px solid var(--warn);
color: var(--muted);
font-size: 0.78rem;
line-height: 1.5;
}
/* The human's own entrant card in the config screen β€” the same .comp-entrant box, marked as the
one that is you. `flex: 0 0 auto` overrides .comp-entrant's own `1 1 260px`, which is sized for
life inside the Quick Duel screen's ROW of two cards: this card is a direct child of
.comp-screen's COLUMN flex instead, where that 260px basis is a height, not a width, and left
the card standing half-empty. */
.comp-human-card { border-color: var(--accent); flex: 0 0 auto; }
.comp-human-name { margin: 0 0 6px; font-size: 1.05rem; font-weight: 700; color: var(--text); }
/* The fixture list: one row per opponent, coloured by what happened. A forfeit reads as a loss
(it is one) but keeps its own italic label, so schedule luck is never mistaken for a fight. */
.comp-fixture-row.is-won td { color: var(--good, #4ade80); }
.comp-fixture-row.is-lost td, .comp-fixture-row.is-forfeit td { color: var(--bad); }
.comp-fixture-row.is-forfeit td:nth-child(4) { font-style: italic; }
.comp-fixture-row.is-next { background: rgba(56, 189, 248, 0.10); }
.comp-fixture-row.is-next td { color: var(--text); font-weight: 600; }
.comp-fixture-row.is-pending td { color: var(--muted); }
/* Attack-move armed: the crosshair cursor alone is easy to miss, so the battlefield
also gets a warm inset outline that unmistakably says "the next click is an attack". */
#field.aim-cursor { cursor: crosshair; box-shadow: inset 0 0 0 2px var(--warn); }
.idle-workers {
font-size: 0.72rem;
color: var(--gold);
background: rgba(255, 209, 102, 0.10);
border: 1px solid var(--gold);
border-radius: 6px;
padding: 3px 9px;
cursor: pointer;
white-space: nowrap;
}
.idle-workers:hover { background: rgba(255, 209, 102, 0.20); }
.idle-workers.hidden { display: none; }
.chip-btn {
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 0.72rem;
padding: 4px 10px;
cursor: pointer;
white-space: nowrap;
}
.chip-btn:hover { border-color: var(--accent); color: var(--accent); }
.btn.resume-btn {
width: auto;
padding: 10px 22px;
border-color: var(--accent);
color: var(--accent);
font-weight: 600;
margin: 0;
}
/* ---------- touch / mobile controls ----------
The field owns every gesture on it (tap / drag / two-finger pan-zoom β€” see
input.js), so tell the browser to keep its hands off: no page scroll, no
pinch-zoom-the-page, no long-press text selection / callout stealing a drag.
The minimap likewise consumes its taps. Everything ELSE (the panel, the setup
screen) keeps normal touch scrolling. */
#field {
touch-action: none;
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
}
.minimap { touch-action: none; }
/* Stop iOS/Android rubber-band bounce dragging the whole game around. */
html, body { overscroll-behavior: none; }
/* Attack-move armed: the Attack-Move HUD button waits for the target tap. */
.btn.armed { border-color: var(--warn); color: var(--warn); background: rgba(251, 191, 36, 0.12); }
.btn.armed:hover { border-color: var(--warn); color: var(--warn); }
/* Help overlay section headings (mouse/keyboard vs touch). */
.help-sub { margin: 16px 0 2px; font-size: 0.85rem; color: var(--accent-2); font-weight: 600; letter-spacing: 0.02em; }
.help-card h2 + .help-sub { margin-top: 2px; }
/* Coarse pointer (finger): grow every tap target to a comfortable size,
independent of screen width, so a phone and a touch-laptop both get big hit
areas without changing the desktop mouse layout. */
@media (pointer: coarse) {
.btn { padding: 12px 12px; font-size: 0.9rem; }
.opt-btn { padding: 11px 14px; min-width: 104px; }
.chip-btn, .mute-btn { padding: 8px 12px; font-size: 0.8rem; }
.help-btn { width: 40px; height: 40px; }
.queue-cancel { padding: 6px 12px; font-size: 0.9rem; }
.idle-workers, .seed-chip { padding: 7px 11px; }
.obj-close { font-size: 1.5rem; padding: 4px; }
.map-card { width: 260px; }
}
/* Narrow screens (phones/tablets in landscape β€” the natural RTS orientation):
trim the side panel and chrome so the battlefield keeps most of the width. */
@media (max-width: 820px) {
#topbar { gap: 6px; padding: 6px 10px; } /* still a two-row column; tighter spacing */
.topbar-row { gap: 8px 10px; }
#topbar .title { font-size: 0.9rem; }
.resources { gap: 10px; font-size: 0.78rem; }
#view { padding: 6px; }
#selectionPanel { width: 200px; padding: 8px; }
.minimap { width: 150px; height: 94px; left: 12px; bottom: 12px; }
.help-key { flex-basis: 130px; }
}
@media (max-width: 600px) {
#topbar .title { display: none; } /* reclaim the width for the resource readout */
#selectionPanel { width: 168px; }
.minimap { width: 118px; height: 74px; }
}
/* A floating handle that collapses the bottom sheet in portrait (wired in
main.js). Hidden everywhere except the portrait layout below. Anchored to the
bottom-right of #view, which sits directly above the sheet, so it rides the
boundary and follows the view when the sheet hides. */
.sheet-toggle {
display: none;
position: absolute;
right: 10px;
bottom: 8px;
z-index: 6;
width: 42px;
height: 42px;
align-items: center;
justify-content: center;
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--accent);
font-size: 1.1rem;
line-height: 1;
cursor: pointer;
}
.sheet-toggle:hover { border-color: var(--accent); }
/* A floating handle that folds the side panel away in the normal (non-portrait) layout β€”
same "reclaim the view" idea as .sheet-toggle above, just for the side-column shape instead
of the portrait bottom sheet. Anchored to the right edge of #view, vertically centred: since
#view is the flexed sibling and #selectionPanel the fixed-width one, right:0 sits exactly on
their shared boundary, so the handle rides that seam and slides out to the window's true right
edge once the panel is hidden β€” the same "follows the panel" trick .sheet-toggle already does
on the opposite (horizontal) seam in portrait. Hidden inside the portrait media query below,
where .sheet-toggle is the one true fold control instead (see main.js: the two toggles clear
each other's collapsed class, so they can never disagree about the panel's visibility). */
.panel-toggle {
display: flex;
position: absolute;
top: 50%;
right: 6px;
transform: translateY(-50%);
z-index: 6;
width: 26px;
height: 46px;
align-items: center;
justify-content: center;
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--accent);
font-size: 1rem;
line-height: 1;
cursor: pointer;
}
.panel-toggle:hover { border-color: var(--accent); }
body.panel-collapsed #selectionPanel { display: none; }
/* Portrait phones / small tablets held upright: stack the layout so the
battlefield spans the full width and the selection panel becomes a BOTTOM
SHEET β€” the standard one-handed mobile shape. The sheet is height-adaptive
(as tall as its content, capped), pairs its buttons two-per-row, and can be
collapsed to reclaim the whole screen with the floating handle. */
@media (orientation: portrait) and (max-width: 820px) {
#main { flex-direction: column; }
/* min-height:0 lets the view shrink below the canvas's intrinsic height so the
bottom sheet gets its share of the column β€” without it the canvas forces the
view full-height and pushes the sheet off the bottom of the screen. */
#view { padding: 4px; min-height: 0; }
#selectionPanel {
width: auto; /* full width instead of a fixed side column */
max-height: 42vh; /* the battlefield keeps the majority of the screen */
flex-shrink: 0;
border-left: none;
border-top: 1px solid var(--border);
border-radius: 12px 12px 0 0;
box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.45);
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 6px 8px;
padding: 10px 10px 14px;
}
/* Text rows / legend span the sheet; buttons pair up two-per-row so it shows
more at a glance and scrolls less. margin:0 β€” the flex `gap` spaces them. */
#selectionPanel > * { flex-basis: 100%; margin: 0; }
#selectionPanel .btn { flex: 1 1 44%; }
.minimap { width: 112px; height: 70px; left: 8px; bottom: 8px; }
.sheet-toggle { display: flex; }
body.sheet-collapsed #selectionPanel { display: none; }
.panel-toggle { display: none; } /* the bottom-sheet handle above is the one fold control here */
}
/* ---------- scenario mode: the convoy status strip + dock actions ---------- */
.scenario-bar {
position: absolute;
top: 12px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
max-width: 94%;
background: rgba(14, 21, 48, 0.94);
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
border-radius: 8px;
padding: 8px 14px;
z-index: 5;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.45);
}
.scenario-bar.hidden { display: none; }
.scenario-banner { font-weight: 700; color: var(--accent); font-size: 0.9rem; }
.scenario-status { color: var(--text); font-size: 0.85rem; font-variant-numeric: tabular-nums; }
.scenario-btn {
background: var(--panel-2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 0.8rem;
padding: 6px 12px;
cursor: pointer;
white-space: nowrap;
}
.scenario-btn:hover { border-color: var(--accent); color: var(--accent); }
.scenario-btn.disabled { opacity: 0.5; cursor: not-allowed; }
.scenario-btn.disabled:hover { border-color: var(--border); color: var(--text); }
.scenario-btn.hidden { display: none; }
/* --- AI Editor (competition.js renderAiEditorScreen) β€” a schema-generated form, so the layout has
to work for any gene the schema grows, not just today's list. --- */
.comp-gene-row {
display: grid;
grid-template-columns: 190px 1fr 52px;
align-items: center;
gap: 10px;
padding: 4px 0;
}
.comp-gene-row > .comp-gene-help { grid-column: 1 / -1; margin: 0 0 4px 190px; font-size: 11px; }
.comp-gene-label { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; }
.comp-gene-value { font-variant-numeric: tabular-nums; text-align: right; font-size: 12px; }
/* INERT: a dial nothing is currently reading. Dimmed rather than hidden or disabled β€” the value is
still real and still saved (it matters again the moment its switch flips back), so hiding it
would lose work and disabling it would suggest it is broken. */
.comp-gene-inert { opacity: 0.45; }
.comp-gene-inert .comp-gene-help { font-style: italic; }
.comp-mix { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.comp-mix-chip { width: auto; padding: 2px 8px; font-size: 11px; }