Spaces:
Paused
Paused
Download style.css from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 71.8 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/style.css
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities@d19cbc4fab45274de920705d8b5b01da170c7e89/style.css
-
curl -L -o style.css https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/d19cbc4fab45274de920705d8b5b01da170c7e89/style.css
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; } | |