Files
Battle-for-Tismo/client/css/style.css
T

1019 lines
31 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
:root {
--hex-w: 63px;
--hex-h: 56px;
--panel-bg: linear-gradient(160deg, rgba(40, 52, 70, 0.96), rgba(18, 24, 34, 0.96));
--panel-border: rgba(150, 190, 240, 0.35);
--text: #e8eef7;
--muted: #9fb0c8;
--accent: #6cb6ff;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
height: 100%;
overflow: hidden;
background: #05070c;
color: var(--text);
font-family: "Segoe UI", "DejaVu Sans", Roboto, sans-serif;
font-size: 14px;
user-select: none;
}
#screens { position: absolute; inset: 0; }
.screen { position: absolute; inset: 0; display: none; }
.screen.active { display: block; }
/* ---------------------------------------------------------------- menus -- */
#screen-menu, #screen-new, #screen-join, #screen-choose {
display: none;
align-items: center;
justify-content: center;
background:
radial-gradient(circle at 50% 20%, rgba(60, 90, 140, 0.35), transparent 60%),
linear-gradient(180deg, #0b1220, #05070c);
}
#screen-menu.active, #screen-new.active, #screen-join.active, #screen-choose.active { display: flex; }
.menu-card {
width: 380px;
padding: 28px 32px;
border-radius: 14px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.08);
text-align: center;
}
.menu-card.wide { width: 520px; text-align: left; }
.menu-card h1 { margin: 0 0 6px; font-size: 30px; letter-spacing: 1px; }
.menu-card.wide h1 { text-align: center; }
.tagline { color: var(--muted); margin: 0 0 24px; }
.field { display: block; margin: 14px 0; }
.field > span:first-child { display: block; color: var(--muted); margin-bottom: 5px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
.field .inline { display: flex; gap: 8px; }
.field .inline input { flex: 1; }
input, select {
width: 100%;
padding: 9px 11px;
border-radius: 7px;
border: 1px solid rgba(160, 190, 230, 0.3);
background: rgba(8, 12, 20, 0.85);
color: var(--text);
font-size: 14px;
outline: none;
}
input:focus, select:focus { border-color: var(--accent); }
.row { display: flex; gap: 10px; justify-content: center; margin-top: 18px; }
.menu-card.wide .row { justify-content: flex-end; }
.aero {
cursor: pointer;
padding: 9px 18px;
border-radius: 8px;
border: 1px solid rgba(150, 190, 240, 0.45);
background: linear-gradient(180deg, rgba(90, 130, 190, 0.55), rgba(35, 55, 90, 0.85));
color: var(--text);
font-size: 14px;
transition: filter 0.12s ease, transform 0.05s ease;
}
.aero:hover { filter: brightness(1.25); }
.aero:active { transform: translateY(1px); }
.aero:disabled { opacity: 0.4; cursor: default; filter: none; }
.aero.primary { background: linear-gradient(180deg, rgba(110, 175, 255, 0.7), rgba(40, 95, 175, 0.9)); }
.aero.big { display: block; width: 100%; margin: 10px 0; padding: 14px; font-size: 17px; }
.status { min-height: 20px; margin: 14px 0 0; color: #ffcf7a; text-align: center; }
.game-list {
list-style: none;
margin: 6px 0 14px;
padding: 0;
max-height: 190px;
overflow-y: auto;
border-radius: 8px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(150, 190, 240, 0.15);
}
.game-list li { padding: 9px 12px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); }
.game-list li:hover { background: rgba(110, 175, 255, 0.15); }
.game-list li.selected { background: rgba(110, 175, 255, 0.3); }
/* Flag + name pickers used on the new-game and join screens. */
.civ-list {
list-style: none;
margin: 6px 0 14px;
padding: 0;
max-height: 220px;
overflow-y: auto;
border-radius: 8px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(150, 190, 240, 0.15);
}
.civ-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 12px;
cursor: pointer;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.civ-list li:hover { background: rgba(110, 175, 255, 0.15); }
.civ-list li.selected { background: rgba(110, 175, 255, 0.3); }
.civ-list img {
width: 32px;
height: 32px;
object-fit: cover;
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.15);
flex: 0 0 auto;
}
/* ------------------------------------------------------------- world map -- */
#screen-game.active { display: block; }
#map-viewport {
position: absolute;
inset: 0;
overflow: hidden;
background-image: url("../assets/map_background.png");
background-size: 512px 512px;
background-color: #05070c;
cursor: grab;
}
#map-viewport.dragging { cursor: grabbing; }
#map-world { position: absolute; left: 0; top: 0; transform-origin: 0 0; z-index: 1; }
/* The WebGL map canvas sits behind the DOM icon layers. It is viewport-sized
and drawn from the same camera, so it never scales with #map-world. */
#map-canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
/* When WebGL owns the map, the DOM geometry layers are left empty and hidden;
the icon layers (paths, targets, entities, labels) still render. */
#map-world.gl-render .detail-layer { display: none; }
/* Promote the map to its own layer only while panning, so a drag is a GPU
translation instead of a repaint of every hex. Keeping it off when idle
avoids holding a huge composited layer in memory. */
#map-viewport.dragging #map-world { will-change: transform; }
.layer { position: absolute; left: 0; top: 0; }
/* Ground layers follow the camera's orthographic tilt (north-south
foreshortening); the sprite and label layers stay upright on top. The factor
is set from CAMERA_TILT by MapView. */
.layer.tilted { transform: scaleY(var(--camera-tilt, 1)); transform-origin: 0 0; }
/* A materialised block of the detailed map. Positioned at its world origin and
moved with `transform`; its tiles are positioned relative to it. */
.chunk { position: absolute; left: 0; top: 0; }
.hex {
position: absolute;
width: var(--hex-w);
height: var(--hex-h);
/* Terrain and fog tiles are drawn from pre-masked art (transparent corners),
so no per-tile clip-path is needed. Clipping thousands of hexes forced a
repaint of the whole map on every pan frame. */
background-repeat: no-repeat;
}
.hex.terrain {
/* Tiling land textures are painted per tile by terrain.js; the atlas only
supplies the hexagonal alpha mask here. */
background-repeat: repeat;
-webkit-mask-image: url("../assets/terrain.png");
-webkit-mask-size: 441px 56px;
-webkit-mask-repeat: no-repeat;
mask-image: url("../assets/terrain.png");
mask-size: 441px 56px;
mask-repeat: no-repeat;
}
.hex.fog {
pointer-events: none;
background-size: var(--hex-w) var(--hex-h);
}
/* The DOM fallback cannot blend fog per vertex, so blur the whole fog layer:
its edge against the visible map fades instead of stepping at the hex line.
The WebGL path hides the layer entirely and does the blur in the shader. */
#layer-fog {
filter: blur(3.5px);
-webkit-filter: blur(3.5px);
}
.fog-unexplored {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='63' height='56'%3E%3Cpolygon points='63,28 47.25,0 15.75,0 0,28 15.75,56 47.25,56' fill='%2304040a'/%3E%3C/svg%3E");
}
.fog-explored {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='63' height='56'%3E%3Cpolygon points='63,28 47.25,0 15.75,0 0,28 15.75,56 47.25,56' fill='%23040612' fill-opacity='0.62'/%3E%3C/svg%3E");
}
.border-seg {
position: absolute;
height: 6px;
border-radius: 3px;
transform-origin: 0 50%;
pointer-events: none;
}
/* Rounds off the junction where two border edges meet, so the outline has no
notches at the corners. */
.border-corner {
position: absolute;
border-radius: 50%;
pointer-events: none;
}
/* A single pre-generated road link, running from tile centre to tile centre. */
.road-seg {
position: absolute;
background: #8a5a2b;
border-radius: 2px;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.6);
transform-origin: 0 50%;
pointer-events: none;
}
/* A ranged unit's bombardment: a red dashed line from the battery to the
target and a red ring around the target. Drawn on the layer below the entity
icons so the units themselves stay on top. */
.target-line {
position: absolute;
height: 0;
border-top: 3px dashed #ff2b2b;
transform-origin: 0 50%;
pointer-events: none;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.7));
}
.target-ring {
position: absolute;
width: 46px;
height: 46px;
margin-left: -23px;
margin-top: -23px;
box-sizing: border-box;
border: 3px solid #ff2b2b;
border-radius: 50%;
pointer-events: none;
box-shadow: 0 0 6px rgba(255, 43, 43, 0.85);
}
/* Outlines the selected or inspected tile. The polygon is built in JS from the
same warped hexagon as the terrain, so it follows the geography. */
.tile-selection {
position: absolute;
width: var(--hex-w);
height: var(--hex-h);
display: none;
pointer-events: none;
}
.tile-selection.active { display: block; }
.tile-selection svg { display: block; width: 100%; height: 100%; overflow: visible; }
.tile-selection polygon {
fill: rgba(255, 255, 255, 0.12);
stroke: rgba(255, 255, 255, 0.9);
stroke-width: 3;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
}
.entity { position: absolute; }
.unit {
--unit-size: 38px;
width: 38px;
height: 38px;
margin-left: -19px;
margin-top: -19px;
cursor: pointer;
}
.unit .unit-statuses,
.city .city-statuses {
position: absolute;
display: flex;
gap: 1px;
pointer-events: none;
z-index: 2;
}
.unit .unit-statuses { top: -4px; right: -4px; }
.city .city-statuses { top: -6px; left: 50%; transform: translateX(-50%); }
.status-icon {
width: calc(var(--unit-size, 38px) / 5);
height: calc(var(--unit-size, 38px) / 5);
display: block;
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.9));
}
.unit .status-icon {
width: calc(var(--unit-size, 38px) / 3);
height: calc(var(--unit-size, 38px) / 3);
}
.city .status-icon {
width: calc(var(--city-size, 26px) / 3);
height: calc(var(--city-size, 26px) / 3);
}
.unit .body {
width: 100%;
height: 100%;
border-radius: 50%;
border: 2px solid rgba(0, 0, 0, 0.65);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.6);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.unit .body img { width: 78%; height: 78%; image-rendering: auto; }
/* Owner's flag centred on the unit's top-left corner, so only a sliver of it
overlaps the unit and the unit icon stays readable. */
.unit .unit-flag {
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 50%;
transform: translate(-50%, -50%);
object-fit: cover;
border: 1px solid rgba(0, 0, 0, 0.65);
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
pointer-events: none;
z-index: 3;
display: block;
}
.unit.selected .body { box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.9), 0 2px 6px rgba(0,0,0,0.6); }
.unit .health {
position: absolute;
left: 3px;
right: 3px;
bottom: -7px;
height: 5px;
background: rgba(30, 0, 0, 0.85);
border: 1px solid rgba(0, 0, 0, 0.7);
border-radius: 3px;
overflow: hidden;
}
.unit .health .fill { height: 100%; background: #46d36a; }
/* Stack size, sitting on the lower-right corner of the unit icon. */
.unit .unit-count {
position: absolute;
right: 0;
bottom: 0;
min-width: 14px;
height: 14px;
padding: 0 3px;
box-sizing: border-box;
border-radius: 7px;
background: #000000;
color: #ffffff;
font-size: 10px;
font-weight: 700;
line-height: 14px;
text-align: center;
pointer-events: none;
z-index: 4;
}
/* On a battlefield both stacks shrink to half size and face each other across
the battle icon. */
.unit.in-battle { transform: scale(0.5); }
.battle-marker {
position: absolute;
width: 22px;
height: 22px;
margin-left: -11px;
margin-top: -11px;
z-index: 4;
pointer-events: none;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9));
}
.battle-marker img { width: 100%; height: 100%; }
.city {
--city-size: 26px;
transform: translate(-50%, -50%);
text-align: center;
}
.city .marker {
width: var(--city-size);
height: var(--city-size);
margin: 0 auto;
border-radius: 5px;
border: 2px solid rgba(255, 255, 255, 0.8);
background-color: #333;
background-size: cover;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.7);
}
/* City names live in their own layer above every unit, marker and status icon,
so they stay legible instead of being covered by whatever is nearby. The
layer needs an explicit z-index because those icons give their own children
z-index 2-4, which would otherwise paint over this layer. */
.city-label-layer { pointer-events: none; z-index: 5; }
.city-label {
position: absolute;
transform: translate(-50%, -100%);
margin-top: -21px;
font-size: 11px;
color: #fff;
text-align: center;
text-shadow: 0 1px 2px #000, 0 0 4px #000;
white-space: nowrap;
pointer-events: none;
}
.city-label.capital::before {
content: "\2605";
color: #ffd54a;
margin-right: 3px;
}
.path-seg {
position: absolute;
height: 4px;
border-radius: 2px;
transform-origin: 0 50%;
pointer-events: none;
opacity: 0.85;
}
/* ------------------------------------------------------------------ HUD -- */
#hud { position: absolute; inset: 0; pointer-events: none; z-index: 10; }
#hud .panel { pointer-events: auto; }
.hidden { display: none !important; }
.panel {
position: absolute;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
#top-panel {
left: 12px;
top: 12px;
min-width: 420px;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 14px;
/* Lifted above the modal overlay so the nation summary — and the approval
figure it now carries — stays bright and readable while a panel is open. */
z-index: 60;
}
.flag-button {
width: 64px;
height: 64px;
padding: 0;
border-radius: 10px;
border: 1px solid var(--panel-border);
background: rgba(0, 0, 0, 0.3);
cursor: pointer;
overflow: hidden;
flex: 0 0 auto;
}
.flag-button img { width: 100%; height: 100%; object-fit: cover; }
.flag-button:hover { filter: brightness(1.2); }
.top-text { flex: 0 0 auto; min-width: 150px; }
.civ-name { font-size: 19px; font-weight: 600; }
.player-name { color: var(--muted); }
.admin-label { color: #ffcf7a; font-size: 12px; }
.top-stats { display: flex; gap: 16px; margin-left: auto; }
.stat { display: flex; flex-direction: column; align-items: flex-end; }
.stat-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
.stat-value { font-size: 15px; font-variant-numeric: tabular-nums; }
/* Approval reads white in the neutral 40–60% band, red when the people
disapprove and green when they approve. */
.stat-value.approval-neutral { color: var(--text); }
.stat-value.approval-positive { color: #6ee787; }
.stat-value.approval-negative { color: #f27272; }
#fps-label {
position: absolute;
right: 14px;
top: 12px;
font-size: 13px;
color: #8fdc8f;
text-shadow: 0 1px 2px #000;
}
/* The inspected tile / city panel is anchored to the bottom right. */
#tile-panel {
right: 14px;
bottom: 14px;
min-width: 320px;
max-width: 560px;
padding: 12px 18px;
}
.info-section.hidden { display: none !important; }
.info-title { font-size: 17px; font-weight: 600; margin-bottom: 4px; }
.tile-coords { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; margin-bottom: 2px; }
.tile-gdp-head { font-size: 13px; font-variant-numeric: tabular-nums; }
.tile-gdp-toggle { cursor: pointer; }
.tile-gdp-toggle:hover { color: #fff; }
.tile-gdp-caret { margin-left: 6px; color: var(--muted); font-size: 11px; }
.tile-gdp-mods { margin-top: 2px; }
.tile-gdp-mod { font-size: 12px; color: var(--muted); padding-left: 12px; font-variant-numeric: tabular-nums; }
#tile-panel .row, #unit-panel .row { justify-content: flex-start; margin-top: 10px; }
.health-row { display: flex; align-items: center; gap: 8px; }
.health-bar { width: 220px; height: 12px; background: rgba(30, 0, 0, 0.8); border-radius: 6px; overflow: hidden; margin-top: 6px; }
#unit-health-fill { height: 100%; background: linear-gradient(180deg, #6fe08a, #2fa04d); }
#unit-health-label { margin-top: 6px; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.unit-stats { display: flex; gap: 16px; margin-top: 6px; }
.unit-stat {
font-size: 13px;
color: var(--muted);
border-bottom: 1px dotted rgba(159, 176, 200, 0.5);
cursor: help;
}
.unit-stat:hover { color: var(--text); }
/* Lower-left dock: the news feed sits above the selected-unit panel, so both
stay clear of the bottom-right tile panel. */
#left-dock {
position: absolute;
left: 14px;
bottom: 14px;
width: 320px;
max-width: calc(50vw - 30px);
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
pointer-events: none;
z-index: 40;
}
#unit-panel {
position: relative;
width: 100%;
padding: 12px 18px;
pointer-events: auto;
}
/* Chat-like feed of short-lived news, with the News button beneath it. */
.game-corner {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
pointer-events: none;
}
.game-feed {
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
}
.game-corner .news-button {
pointer-events: auto;
padding: 6px 14px;
font-size: 13px;
}
.feed-line {
align-self: flex-start;
max-width: 100%;
padding: 6px 10px;
border-radius: 7px;
background: rgba(8, 12, 20, 0.86);
border: 1px solid rgba(150, 190, 240, 0.35);
color: var(--text);
font-size: 12px;
line-height: 1.35;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
opacity: 1;
transform: translateY(0);
transition: opacity 0.4s ease, transform 0.4s ease;
animation: feed-in 0.18s ease;
}
.feed-line.leaving { opacity: 0; transform: translateY(8px); }
.feed-line.feed-alert {
background: rgba(80, 14, 14, 0.92);
border-color: #ff6a6a;
color: #ffd9d9;
font-weight: 600;
}
@keyframes feed-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.tooltip {
position: absolute;
z-index: 80;
max-width: 280px;
padding: 6px 10px;
border-radius: 7px;
background: rgba(8, 12, 20, 0.97);
border: 1px solid var(--panel-border);
color: var(--text);
font-size: 12px;
white-space: nowrap;
pointer-events: none;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.6);
}
/* Right-click menu for the units sharing a tile. */
.stack-menu {
position: absolute;
z-index: 70;
min-width: 200px;
padding: 4px;
border-radius: 8px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
display: flex;
flex-direction: column;
gap: 2px;
}
.stack-item {
text-align: left;
padding: 7px 10px;
border: none;
border-radius: 5px;
background: transparent;
color: var(--text);
font-size: 13px;
cursor: pointer;
}
.stack-item:hover { background: rgba(110, 175, 255, 0.2); }
.stack-item.stack-all {
margin-top: 2px;
border-top: 1px solid rgba(255, 255, 255, 0.12);
color: #ffcf7a;
}
/* --------------------------------------------------------------- modals -- */
.modal {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.modal.hidden { display: none; }
/* The city and government panels dock below the top-left nation summary
instead of covering it, so the approval figure stays in view while they are
open. The 104px clears the summary's 12px offset plus its padded height. */
#modal-city,
#modal-nation {
align-items: flex-start;
padding: 104px 16px 16px;
}
.modal-card {
width: 620px;
max-height: 80vh;
display: flex;
flex-direction: column;
border-radius: 14px;
background: var(--panel-bg);
border: 1px solid var(--panel-border);
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.7);
overflow: hidden;
}
.modal-card-small { width: 420px; }
.modal-card-small .row { padding-bottom: 18px; }
/* The nation panel carries wide tables and long policy/campaign rows, so it
takes most of the screen. A fixed height keeps it from resizing as tabs with
wildly different content are switched. */
#modal-nation .modal-card {
width: 90vw;
max-width: 1600px;
height: min(80vh, 900px);
max-height: calc(100vh - 120px);
}
.modal-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 18px;
font-size: 19px;
font-weight: 600;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.modal-sub { padding: 10px 18px; color: var(--muted); }
.modal-body { padding: 12px 18px 18px; overflow-y: auto; }
.modal-close { padding: 4px 11px; }
.tabs {
display: flex;
/* Wrap rather than scroll: every tab stays visible with no clipped edge,
and the bar keeps a stable height whatever tab is selected. */
flex-wrap: wrap;
flex: 0 0 auto;
gap: 6px;
padding: 10px 18px 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.tab {
cursor: pointer;
flex: 0 0 auto;
white-space: nowrap;
padding: 8px 16px;
border-radius: 8px 8px 0 0;
border: 1px solid transparent;
border-bottom: none;
background: rgba(0, 0, 0, 0.25);
color: var(--muted);
}
.tab.active { background: rgba(110, 175, 255, 0.22); color: var(--text); }
.tab-page { display: none; }
/* The active tab fills the rest of the card and lets its body scroll: without
min-height: 0 the flex item would grow past the card and be clipped by the
card's overflow: hidden, hiding e.g. long diplomacy tables. */
.tab-page.active { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.tab-page.active > .modal-sub { flex: 0 0 auto; }
.tab-page.active > .modal-body { flex: 1 1 auto; min-height: 0; }
.row-card {
display: flex;
gap: 14px;
padding: 10px 12px;
margin-bottom: 8px;
border-radius: 9px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.row-card .info { flex: 1; }
.row-card .building-icon {
width: 46px;
height: 46px;
flex: 0 0 auto;
align-self: center;
opacity: 0.9;
}
.unit-icon {
width: 26px;
height: 26px;
flex: 0 0 auto;
opacity: 0.9;
}
.row-card .name { font-weight: 600; }
.row-card .desc { color: var(--muted); font-size: 12px; margin: 3px 0; }
.row-card .effects { font-size: 12px; color: #bcd4ff; }
.row-card .cost { font-size: 12px; color: var(--muted); margin-top: 3px; }
.row-card .actions { display: flex; flex-direction: column; gap: 6px; justify-content: center; }
.row-card .actions .aero { min-width: 170px; }
.row-card .value {
display: flex;
align-items: flex-start;
justify-content: flex-end;
min-width: 140px;
font-weight: 600;
}
.row-card .value.positive { color: #8fe3a8; }
.row-card .value.negative { color: #ff9a9a; }
.row-card .budget-detail { margin-top: 4px; }
.row-card .budget-line { font-size: 12px; color: var(--muted); margin-top: 2px; }
.row-card.conflict-card { display: block; cursor: pointer; }
.row-card.conflict-card:hover { background: rgba(255, 255, 255, 0.09); }
.conflict-detail {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.conflict-detail .conflict-line { font-size: 12px; color: var(--muted); }
.conflict-detail .conflict-side { font-size: 13px; margin-top: 4px; }
.diplomacy-table { width: 100%; border-collapse: collapse; }
.diplomacy-table th,
.diplomacy-table td {
padding: 8px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
text-align: left;
}
.diplomacy-table th {
cursor: pointer;
user-select: none;
color: var(--muted);
font-weight: 600;
white-space: nowrap;
}
.diplomacy-table th:hover { color: var(--text); }
.diplomacy-table th.sorted-asc,
.diplomacy-table th.sorted-desc { color: var(--text); }
.diplomacy-table th.numeric,
.diplomacy-table td.numeric { text-align: right; }
.diplomacy-table .sort-arrow { font-size: 10px; margin-left: 4px; }
.diplomacy-table tbody tr:hover { background: rgba(255, 255, 255, 0.05); }
.diplomacy-table tbody tr.local-nation { background: rgba(120, 170, 255, 0.16); }
.diplomacy-table tbody tr.local-nation:hover { background: rgba(120, 170, 255, 0.24); }
.diplomacy-table tbody tr.local-nation td.name { font-weight: 600; }
.diplomacy-table td.status { white-space: nowrap; }
.diplomacy-table td.status .aero { margin-left: 8px; padding: 4px 10px; }
/* Budget table: income green, every cost red, alternating rows and the net
total pinned to the last row. */
.budget-table { width: 100%; border-collapse: collapse; }
.budget-table th,
.budget-table td {
padding: 7px 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
text-align: left;
}
.budget-table th { color: var(--muted); font-weight: 600; }
.budget-table td.numeric,
.budget-table th.numeric { text-align: right; }
.budget-table tbody tr:nth-child(even) { background: rgba(255, 255, 255, 0.045); }
.budget-table tbody tr:hover { background: rgba(110, 175, 255, 0.12); }
.budget-table .budget-value,
.budget-table td.numeric { text-align: right; font-variant-numeric: tabular-nums; }
.budget-table tr.income .budget-value,
.budget-table tr.income-detail .budget-value,
.budget-table td.income { color: #8fe3a8; }
.budget-table tr.budget-group .budget-value,
.budget-table tr.expense .budget-value,
.budget-table tr.expense-detail .budget-value { color: #ff9a9a; }
.budget-table tr.budget-section td {
padding-top: 14px;
color: var(--muted);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.budget-table tr.budget-group .budget-name.clickable { cursor: pointer; }
.budget-table tr.budget-group .budget-name.clickable:hover { color: var(--accent); }
.budget-caret { display: inline-block; width: 12px; color: var(--muted); }
.budget-note { margin-top: 2px; color: var(--muted); font-size: 11px; }
.budget-table tfoot .budget-total td {
padding-top: 10px;
border-top: 2px solid rgba(255, 255, 255, 0.25);
font-weight: 700;
}
.budget-table tfoot .budget-total .budget-value { color: #fff; }
.training-status { padding: 0 18px 16px; }
.training-queue { padding-top: 0; }
.training-queue .queue-empty { padding: 4px 0; }
.training-queue .queue-row { padding: 8px 12px; }
.training-queue .queue-row .actions .aero { min-width: 110px; }
/* The city modal keeps a fixed size whatever tab is open, so switching between
train, buildings, people and budget never resizes the card. The unit buttons
sit in a fixed-height box that scrolls when there are more units than fit;
the queue below is the flexible part and scrolls too, so adding units never
stretches the card or pushes the buttons out of reach. */
#modal-city .modal-card {
width: 680px;
max-width: calc(100vw - 32px);
height: min(80vh, 740px);
max-height: calc(100vh - 120px);
}
#modal-city #city-train-list {
flex: 0 0 auto;
width: 100%;
max-height: 168px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
#modal-city #city-train-list .train-button {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 10px;
text-align: left;
}
/* Local test game: the free, instant twin of an order button. */
.free-button {
border: 1px dashed rgba(130, 225, 170, 0.75) !important;
color: #b8f5cd;
}
.free-button:disabled { opacity: 0.4; }
#modal-city .training-status { flex: 0 0 auto; }
#modal-city .training-queue { flex: 1 1 auto; min-height: 0; }
.progress { height: 14px; margin-top: 8px; background: rgba(0, 0, 0, 0.5); border-radius: 7px; overflow: hidden; }
#city-train-progress { height: 100%; width: 0; background: linear-gradient(180deg, #7fc4ff, #2f7fd0); }
.news-item {
padding: 10px 12px;
margin-bottom: 8px;
border-radius: 9px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.news-item .news-text { font-weight: 600; }
.news-item .news-date { color: var(--muted); font-size: 12px; margin-top: 3px; }
.news-empty { color: var(--muted); padding: 8px 0; }
#news-button { flex: 0 0 auto; }
.banner {
position: absolute;
left: 50%;
top: 12px;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 8px;
background: rgba(140, 20, 20, 0.9);
border: 1px solid rgba(255, 120, 120, 0.6);
z-index: 60;
}
/* Politics: the nation Politics tab and the city People tab. */
.politics-card {
padding: 12px;
margin-bottom: 10px;
border-radius: 9px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.politics-card > .name { font-weight: 700; margin-bottom: 4px; }
.politics-controls {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 10px;
margin-top: 8px;
}
.politics-field {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
}
.politics-field .politics-label { color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.politics-select {
background: rgba(0, 0, 0, 0.4);
color: inherit;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
padding: 4px 6px;
}
.politics-readout { color: var(--muted); font-size: 12px; margin-left: 8px; }
.politics-policy {
padding: 8px 0;
border-top: 1px solid rgba(255, 255, 255, 0.07);
}
.politics-policy:first-of-type { border-top: none; }
.politics-row {
padding: 4px 0;
font-size: 13px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.politics-penalty { color: #f0b060; }
.ethnic-chart { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-top: 8px; }
.ethnic-legend { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
.ethnic-legend-row { display: flex; align-items: center; gap: 8px; }
.ethnic-swatch { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
.ethnic-donut { flex: 0 0 auto; }
.politics-policy-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.politics-new { margin-top: 10px; }
.politics-wizard {
margin-top: 10px;
padding: 12px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.28);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.politics-estimate { margin-top: 8px; font-size: 13px; }
.politics-cost { color: #f0b060; }
.politics-wizard .politics-controls { margin-top: 0; }
.ethnic-trend { font-weight: 700; margin-left: 2px; }
.trend-up { color: #6ee787; }
.trend-down { color: #f27272; }
#tile-ethnicity { margin-top: 6px; }
#tile-ethnicity .tile-population { font-size: 13px; font-variant-numeric: tabular-nums; }
#tile-ethnicity .ethnic-chart { margin-top: 4px; gap: 10px; }
#tile-ethnicity .ethnic-legend { font-size: 12px; }
.politics-row-actions { display: flex; justify-content: flex-end; margin-top: 4px; }
.politics-spend { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.politics-slider { flex: 1 1 auto; accent-color: #7fc4ff; }
.opinion-list { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }
.opinion-heading {
margin-top: 8px;
color: var(--muted);
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.05em;
}
.opinion-row {
display: grid;
grid-template-columns: 170px 1fr 48px;
align-items: center;
gap: 8px;
font-size: 13px;
}
.opinion-bar { height: 10px; border-radius: 5px; background: rgba(0, 0, 0, 0.4); overflow: hidden; }
.opinion-fill { height: 100%; background: #6ee787; }
.opinion-fill.negative { background: #f27272; }
.opinion-pct { text-align: right; color: var(--muted); }