: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; } /* Boot-style loading log, sitting over every screen while a world is built. */ .loading-overlay { position: absolute; inset: 0; z-index: 200; display: flex; align-items: flex-start; justify-content: flex-start; padding: 28px 34px; background: radial-gradient(circle at 30% 0%, rgba(40, 70, 110, 0.25), transparent 60%), #05070c; overflow: hidden; } .loading-overlay.hidden { display: none; } /* Once finished the overlay only fades; it must not keep swallowing clicks meant for the HUD underneath it. */ .loading-overlay.done { opacity: 0.0; transition: opacity 0.6s ease; pointer-events: none; } .loading-panel { width: min(720px, 100%); max-height: 100%; display: flex; flex-direction: column; gap: 10px; } .loading-title { font-size: 13px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent); } .loading-log { overflow: hidden auto; padding: 12px 14px; border-radius: 10px; border: 1px solid rgba(120, 200, 255, 0.25); background: rgba(4, 8, 14, 0.9); box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.6); font-family: "DejaVu Sans Mono", "Consolas", "Courier New", monospace; font-size: 12.5px; line-height: 1.55; color: #b9d4e8; white-space: pre; } .loading-line { color: #9fc3dc; } .loading-line.loading-total { margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(120, 200, 255, 0.2); color: #ffd77a; font-weight: 600; } /* ---------------------------------------------------------------- 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 road or railway link, running from tile centre to tile centre. The texture (and its repeat) is set in JS; only the quad's shape is styled here. */ .road-seg, .rail-seg { position: absolute; background-color: transparent; transform-origin: 50% 0; pointer-events: none; image-rendering: pixelated; } /* A ranged unit's bombardment: a red dashed parabolic arc 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-arcs { position: absolute; left: 0; top: 0; width: 1px; height: 1px; overflow: visible; pointer-events: none; } .target-line { fill: none; stroke: #ff2b2b; stroke-width: 3; stroke-dasharray: 10 8; stroke-linecap: round; pointer-events: none; opacity: 0.6; filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.7)); } .target-shell { fill: #ffdd66; stroke: #ff2b2b; stroke-width: 1; pointer-events: none; filter: drop-shadow(0 0 3px rgba(255, 90, 40, 0.9)); } .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; opacity: 0.6; box-shadow: 0 0 6px rgba(255, 43, 43, 0.85); } /* A barrage the player is looking at stands out from the rest. */ .target-line.target-selected, .target-ring.target-selected { opacity: 1; } /* 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: none; } .tile-selection path { fill: none; stroke: rgba(255, 255, 255, 0.9); stroke-width: 3; stroke-linejoin: round; vector-effect: non-scaling-stroke; } .tile-selection .tile-selection-hidden { stroke-dasharray: 6 5; } .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); } /* A trench: a hatched overlay marking the dug-in tile. */ .trench-marker { position: absolute; width: 34px; height: 30px; margin-left: -17px; margin-top: -15px; background-image: repeating-linear-gradient( 45deg, rgba(90, 60, 30, 0.55) 0 3px, rgba(40, 26, 12, 0.55) 3px 6px ); border: 1px solid rgba(0, 0, 0, 0.4); pointer-events: none; z-index: 3; } /* A military tile improvement marker. */ .tile-improvement-marker { position: absolute; width: 22px; height: 22px; margin-left: -11px; margin-top: -11px; pointer-events: none; z-index: 3; filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)); } .tile-improvement-marker img { width: 100%; height: 100%; } .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. */ /* Country names for the far-out political map, drawn as stretched SVG text on their own layer below the city labels. The SVG has no size of its own; the overflow is visible so the rotated text can sit anywhere on the map. */ /* No z-index on purpose: the layer is first of the icon layers in the DOM, so it paints beneath the selection, paths, targets, units, cities and labels. */ .country-label-layer { pointer-events: none; } .political-label-svg { position: absolute; left: 0; top: 0; width: 0; height: 0; overflow: visible; } .political-label-svg text { font-family: inherit; font-weight: 700; pointer-events: none; } /* On the flat political and economic maps the transport network stays visible, but as solid strokes instead of the road/rail textures. */ #map-world.political .road-seg, #map-world.economic .road-seg { background-image: none !important; background-color: #3a3a3d; } #map-world.political .rail-seg, #map-world.economic .rail-seg { background-image: none !important; background-color: #8a5a2b; } .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; } /* A discreet progress bar under the city name, showing the first queued build (yellow) or unit (blue). Hidden while the city is idle. */ .city-progress { position: absolute; left: 50%; bottom: 100%; margin-bottom: 1px; transform: translateX(-50%); width: var(--city-size); height: 3px; box-sizing: border-box; background: rgba(0, 0, 0, 0.6); border: 1px solid rgba(0, 0, 0, 0.7); border-radius: 2px; overflow: hidden; pointer-events: none; z-index: 4; } .city-progress .fill { height: 100%; width: 0; background: #7fc4ff; } .city-progress.building .fill { background: #ffd54a; } /* One small square per building level, scattered around the city icon. */ .city-buildings { position: absolute; inset: 0; pointer-events: none; z-index: 1; } .city-building-square { position: absolute; width: 10%; height: 10%; transform: translate(-50%, -50%); box-sizing: border-box; background: rgba(255, 255, 255, 0.85); border: 1px solid rgba(0, 0, 0, 0.6); border-radius: 1px; } .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; } /* Month-on-month change marker inside a stat: green up, red down, neutral for unchanged, doubled for a move past the indicator's threshold. */ .stat-arrow { margin-left: 4px; font-size: 11px; font-weight: 700; letter-spacing: -1px; } .stat-arrow.up { color: #6ee787; } .stat-arrow.down { color: #f27272; } .stat-arrow.equal { color: var(--muted); } /* 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; } /* Colour scale for the economic map modes, in its own space below the FPS readout. The gradient itself is painted from the shared ramp in JS. */ .map-legend { position: absolute; right: 14px; top: 38px; width: 190px; padding: 10px 12px; border-radius: 10px; background: var(--panel-bg); border: 1px solid var(--panel-border); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55); pointer-events: none; z-index: 30; } .map-legend-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin-bottom: 6px; } .map-legend-bar { height: 12px; border-radius: 6px; border: 1px solid rgba(0, 0, 0, 0.5); } .map-legend-scale { display: flex; justify-content: space-between; margin-top: 4px; font-size: 11px; color: var(--text); font-variant-numeric: tabular-nums; } /* 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; } /* One button per buildable tile improvement, laid out in a wrapping row. */ .tile-improvement-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } .improvement-btn { font-size: 12px; } .improvement-btn.current { opacity: 0.6; } .improvement-btn.remove { border-color: rgba(242, 114, 114, 0.6); } /* Buildable improvements are square icons with the cost written beneath. */ .improvement-icon-btn { display: flex; flex-direction: column; align-items: center; gap: 2px; width: 64px; padding: 5px 4px; } .improvement-icon { width: 40px; height: 40px; display: block; } .improvement-cost { font-size: 10px; color: var(--muted); font-variant-numeric: tabular-nums; } .info-section.hidden { display: none !important; } .info-title { font-size: 17px; font-weight: 600; margin-bottom: 4px; } .city-production-label { font-size: 12px; color: var(--muted); margin-top: 4px; } .city-production-bar { height: 8px; margin-top: 4px; } .city-production-bar > div { height: 100%; background: linear-gradient(180deg, #7fc4ff, #2f7fd0); } .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-cargo { margin-top: 6px; font-size: 12px; color: var(--muted); } .unit-stack-list { display: flex; flex-direction: column; gap: 3px; margin-top: 8px; max-height: 140px; overflow-y: auto; } .stack-unit { display: flex; align-items: center; gap: 8px; font-size: 12px; cursor: pointer; } .stack-unit-name { flex: 1; } .stack-unit-hp { 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; } .dock-buttons { position: relative; display: flex; gap: 8px; pointer-events: auto; } .game-corner .news-button { pointer-events: auto; padding: 6px 14px; font-size: 13px; } /* The map-mode picker, anchored directly above the buttons. */ .map-modes-popover { position: absolute; left: 0; bottom: calc(100% + 8px); min-width: 170px; padding: 8px; display: flex; flex-direction: column; gap: 2px; border-radius: 10px; background: var(--panel-bg); border: 1px solid var(--panel-border); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55); pointer-events: auto; z-index: 45; } .map-modes-title { padding: 2px 8px 6px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); } .map-mode-option { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; font-size: 13px; color: var(--text); cursor: pointer; } .map-mode-option:hover { background: rgba(110, 175, 255, 0.2); } .map-mode-option input { accent-color: #6eafff; cursor: pointer; } .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; } /* Capability report: one row per WebGL/WASM feature with a status badge. */ .capability-row { display: flex; align-items: flex-start; gap: 10px; padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.06); } .capability-status { flex: 0 0 auto; min-width: 68px; text-align: center; padding: 2px 6px; border-radius: 6px; font-size: 11px; letter-spacing: 0.06em; font-weight: 600; background: rgba(110, 175, 255, 0.18); color: var(--muted); } .capability-pass .capability-status { background: rgba(90, 200, 120, 0.2); color: #8fe0a3; } .capability-fail .capability-status { background: rgba(220, 90, 90, 0.22); color: #ff9a9a; } .capability-optional .capability-status { background: rgba(230, 180, 90, 0.2); color: #ffcf7a; } .capability-info .capability-status { background: rgba(150, 160, 180, 0.18); color: var(--muted); } .capability-label { font-weight: 600; } .capability-detail { color: var(--muted); font-size: 12px; margin-top: 2px; } .capability-requirements { margin: 0; padding-left: 40px; list-style: disc; } .capability-requirements li { margin-bottom: 6px; color: var(--muted); font-size: 13px; } /* Let the checks scroll while the card stays inside the viewport. */ #capabilities-list { flex: 1 1 auto; min-height: 0; } #capabilities-min { flex: 0 0 auto; max-height: 28vh; } .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); } /* Secondary tabs inside a page, e.g. the technology themes. */ .subtabs { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 18px 8px; } .subtab { cursor: pointer; white-space: nowrap; padding: 5px 12px; border-radius: 7px; border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(0, 0, 0, 0.25); color: var(--muted); font-size: 12px; } .subtab.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; } /* Technology cards are wide, short 3:1 tiles in a grid, so the tab shows many more of them at once than a single stacked list would. */ #technology-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 8px; align-content: start; } #technology-list .row-card { margin-bottom: 0; aspect-ratio: 3 / 1; overflow: hidden; padding: 8px 10px; gap: 10px; } #technology-list .row-card .desc, #technology-list .row-card .effects { margin: 2px 0; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } #technology-list .row-card .actions { justify-content: flex-start; align-items: flex-start; } #technology-list .row-card .actions .aero { min-width: 0; padding: 3px 12px; } /* A focused technology fills a bar with the points committed beside it. */ #technology-list .cost { display: flex; align-items: center; gap: 8px; } .tech-progress-track { flex: 1 1 auto; height: 8px; background: rgba(255, 255, 255, 0.14); border-radius: 4px; overflow: hidden; } .tech-progress-fill { height: 100%; background: linear-gradient(90deg, #4f8cff, #7fd1ff); } .tech-progress-text { white-space: nowrap; font-variant-numeric: tabular-nums; } .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); }