tower / files / src / style.css
style.css
09a64747 2026-09-07 Pavel Safronov | 1 | :root { |
| 2 | --bg: #0b0d12; --panel: #131722; --line: #232a3a; | |
9d5a061c 2026-09-27 Pavel Safronov | 3 | --fg: #dde3ee; --dim: #8892a6; --accent: #7fd4ff; |
09a64747 2026-09-07 Pavel Safronov | 4 | color-scheme: dark; |
| 5 | } | |
| 6 | * { box-sizing: border-box; } | |
| 7 | html, body { height: 100%; margin: 0; } | |
| 8 | body { | |
| 9 | background: var(--bg); color: var(--fg); | |
| 10 | font: 13px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; | |
| 11 | } | |
| 12 | #app { display: grid; grid-template-columns: 1fr 340px; height: 100%; } | |
| 13 | main { position: relative; overflow: hidden; } | |
5f38efd0 2026-09-07 Pavel Safronov | 14 | canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; |
| 15 | cursor: crosshair; touch-action: none; } | |
7cc177f6 2026-09-07 Pavel Safronov | 16 | #exterior, #interior { cursor: grab; } |
| 17 | #exterior:active, #interior:active { cursor: grabbing; } | |
5f38efd0 2026-09-07 Pavel Safronov | 18 | .views { |
| 19 | position: absolute; top: 12px; left: 12px; display: flex; gap: 0; | |
| 20 | border: 1px solid var(--line); border-radius: 6px; overflow: hidden; | |
| 21 | background: rgba(10,13,20,0.86); | |
| 22 | } | |
| 23 | .views button { | |
| 24 | width: auto; margin: 0; border: none; border-radius: 0; padding: 6px 14px; | |
| 25 | background: transparent; font-size: 12px; | |
| 26 | } | |
| 27 | .views button.active { background: #1d2942; color: var(--accent); } | |
| 28 | #spin { left: auto; right: 12px; bottom: auto; top: 12px; } | |
8b6287ba 2026-09-07 Pavel Safronov | 29 | .labels3d { position: absolute; inset: 0; pointer-events: none; overflow: hidden; } |
1c0284d0 2026-09-27 Pavel Safronov | 30 | /* Show labels, off: the 3D views' captions go; the map stops drawing its names (map.ts). */ |
| 31 | #app.no-labels .labels3d { display: none; } | |
8b6287ba 2026-09-07 Pavel Safronov | 32 | .labels3d span { |
| 33 | position: absolute; transform: translate(-50%, -50%); white-space: nowrap; | |
| 34 | font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; | |
| 35 | color: rgba(226,236,250,0.9); text-shadow: 0 0 4px #05070d, 0 0 8px #05070d; | |
| 36 | letter-spacing: 0.04em; | |
| 37 | } | |
| 38 | .labels3d span small { display: block; color: var(--dim); letter-spacing: 0; } | |
7cc177f6 2026-09-07 Pavel Safronov | 39 | #int-labels span:hover { color: var(--accent); } |
09a64747 2026-09-07 Pavel Safronov | 40 | |
| 41 | .overlay { | |
| 42 | position: absolute; bottom: 12px; left: 12px; | |
| 43 | background: rgba(10,13,20,0.86); border: 1px solid var(--line); border-radius: 6px; | |
| 44 | padding: 8px 10px; font: 11px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; | |
| 45 | pointer-events: none; white-space: pre; | |
| 46 | } | |
| 47 | #sightline { left: auto; right: 12px; } | |
3e898b51 2026-09-28 Pavel Safronov | 48 | |
| 49 | /* A readout that can be folded down to its title (D-082). The box itself stays | |
| 50 | click-through, as every overlay is; only the title takes the click. */ | |
| 51 | .overlay.collapsible { padding: 0; white-space: normal; } | |
| 52 | .overlay.collapsible > div { white-space: pre; } | |
| 53 | .overlay-head { | |
| 54 | display: flex; align-items: center; gap: 8px; width: 100%; margin: 0; | |
| 55 | padding: 6px 10px; border: none; border-radius: 6px; background: none; | |
| 56 | font: inherit; color: var(--dim); text-align: left; pointer-events: auto; | |
| 57 | text-transform: uppercase; letter-spacing: 0.08em; font-size: 10px; | |
| 58 | } | |
| 59 | .overlay-head:hover { background: none; color: var(--fg); } | |
| 60 | .overlay-head::after { | |
| 61 | content: ''; margin-left: auto; width: 5px; height: 5px; | |
| 62 | border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; | |
| 63 | transform: rotate(-135deg) translate(-1px, -1px); | |
| 64 | } | |
| 65 | .overlay-head[aria-expanded=false]::after { transform: rotate(45deg) translate(-2px, -2px); } | |
| 66 | .overlay.collapsible > div { padding: 0 10px 8px; } | |
| 67 | .overlay.collapsed > div { display: none; } | |
| 68 | ||
| 69 | /* The eye-height gauge (D-082): an altimeter up the right of the interior view, log | |
| 70 | scaled, with things of known height marked on it. */ | |
| 71 | .gauge { | |
| 72 | position: absolute; right: 18px; top: 64px; bottom: 40px; width: 10px; | |
| 73 | font: 10px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; | |
| 74 | } | |
| 75 | .gauge-track { | |
| 76 | position: absolute; inset: 0; border-radius: 5px; cursor: ns-resize; touch-action: none; | |
| 77 | background: rgba(10,13,20,0.7); border: 1px solid var(--line); outline: none; | |
| 78 | } | |
| 79 | .gauge-track:focus-visible { border-color: var(--accent); } | |
| 80 | .gauge-mark { | |
| 81 | position: absolute; right: 12px; transform: translateY(50%); | |
| 82 | display: flex; align-items: center; gap: 6px; white-space: nowrap; pointer-events: none; | |
| 83 | color: rgba(190,200,216,0.8); | |
| 84 | text-shadow: 0 0 2px #05070d, 0 0 4px #05070d, 0 0 8px #05070d; | |
| 85 | } | |
| 86 | .gauge-mark i { font-style: normal; opacity: 0.75; } | |
| 87 | /* Too close to the mark below it at this height of screen: tick only. */ | |
| 88 | .gauge-mark.crowded span, .gauge-mark.crowded i { display: none; } | |
| 89 | .gauge-mark::after { content: ''; width: 8px; border-top: 1px solid currentColor; } | |
| 90 | .gauge-mark.passed { color: #eef2f8; } | |
| 91 | .gauge-mark.level { color: var(--accent); } | |
| 92 | .gauge-thumb { | |
| 93 | position: absolute; left: 50%; width: 16px; height: 16px; margin-left: -8px; | |
| 94 | transform: translateY(50%); border-radius: 50%; pointer-events: none; | |
| 95 | background: var(--accent); box-shadow: 0 0 0 3px rgba(10,13,20,0.9); | |
| 96 | } | |
| 97 | /* The height itself, beside the thumb and clear of the marks, on the view's side. */ | |
| 98 | .gauge-readout { | |
| 99 | position: absolute; right: 22px; top: 50%; transform: translateY(-50%); | |
| 100 | white-space: nowrap; text-align: right; padding: 4px 8px; border-radius: 5px; | |
| 101 | background: rgba(10,13,20,0.92); border: 1px solid var(--accent); color: var(--fg); | |
| 102 | font-size: 11px; | |
| 103 | } | |
| 104 | .gauge-readout span { color: var(--accent); } | |
09a64747 2026-09-07 Pavel Safronov | 105 | .hidden { display: none !important; } |
| 106 | ||
376f2ce6 2026-09-16 Pavel Safronov | 107 | /* The status dialog (world-generation.md §10.1). Over the world, never over the panel: |
| 108 | the author can keep reading notes while the world is made. */ | |
| 109 | .status[hidden] { display: none; } | |
| 110 | .status { | |
| 111 | position: absolute; left: 50%; top: 42%; transform: translate(-50%, -50%); | |
| 112 | width: min(420px, calc(100% - 32px)); padding: 18px 20px 20px; box-sizing: border-box; | |
| 113 | background: rgba(11, 13, 18, 0.92); border: 1px solid var(--line); border-radius: 8px; | |
| 114 | text-align: center; z-index: 5; pointer-events: none; | |
| 115 | } | |
| 116 | .status p { margin: 0; } | |
| 117 | .status-title { font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--dim); } | |
| 118 | .status-line { font-size: 15px; margin: 10px 0 14px !important; min-height: 1.3em; } | |
| 119 | .status-bar { height: 3px; background: #1b2130; border-radius: 2px; overflow: hidden; } | |
| 120 | #status-fill { height: 100%; width: 0; background: var(--accent); transition: width 0.25s ease-out; } | |
| 121 | ||
09a64747 2026-09-07 Pavel Safronov | 122 | aside { |
| 123 | background: var(--panel); border-left: 1px solid var(--line); | |
| 124 | overflow-y: auto; padding: 0 14px 24px; | |
| 125 | } | |
0e0c2dba 2026-09-07 Pavel Safronov | 126 | header { display: flex; align-items: center; gap: 8px; padding: 14px 0 10px; } |
09a64747 2026-09-07 Pavel Safronov | 127 | h1 { font-size: 15px; margin: 0; letter-spacing: 0.08em; text-transform: uppercase; } |
b4b8e46c 2026-09-28 Pavel Safronov | 128 | /* The branch picker: only there when the site was built with more than one branch. */ |
| 129 | #branch { width: auto; margin: 0 0 0 auto; padding: 3px 6px; font-size: 11px; } | |
0e0c2dba 2026-09-07 Pavel Safronov | 130 | |
| 131 | /* One section open at a time; the summary is the whole affordance. */ | |
| 132 | details.sec { border-top: 1px solid var(--line); } | |
| 133 | details.sec[hidden] { display: none; } | |
| 134 | details.sec > summary { | |
| 135 | display: flex; align-items: center; gap: 8px; cursor: pointer; | |
| 136 | padding: 12px 0; list-style: none; | |
| 137 | font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--dim); | |
| 138 | } | |
| 139 | details.sec > summary::-webkit-details-marker { display: none; } | |
| 140 | details.sec > summary::after { | |
| 141 | content: ''; margin-left: auto; width: 6px; height: 6px; | |
| 142 | border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; | |
| 143 | transform: rotate(45deg) translate(-2px, -2px); transition: transform 0.12s ease; | |
| 144 | } | |
| 145 | details.sec[open] > summary { color: var(--fg); } | |
| 146 | details.sec[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); } | |
| 147 | details.sec > summary:hover { color: var(--fg); } | |
| 148 | details.sec > :not(summary):last-child { margin-bottom: 14px; } | |
09a64747 2026-09-07 Pavel Safronov | 149 | .dim { color: var(--dim); font-weight: 400; } |
| 150 | .hint { color: var(--dim); font-size: 11px; margin: 8px 0 14px; } | |
| 151 | code { background: #0b0d12; padding: 1px 4px; border-radius: 3px; } | |
| 152 | ||
| 153 | button { | |
| 154 | width: 100%; margin-bottom: 6px; padding: 7px 10px; border-radius: 6px; | |
| 155 | border: 1px solid var(--line); background: #1b2130; color: var(--fg); | |
| 156 | font: inherit; font-size: 12px; cursor: pointer; | |
| 157 | } | |
| 158 | button:hover { background: #222a3d; } | |
7cc177f6 2026-09-07 Pavel Safronov | 159 | button:disabled { opacity: 0.45; cursor: default; } |
2bb3e0de 2026-09-08 Pavel Safronov | 160 | /* The Light menu is disabled while the clock is driving the light; say so. */ |
| 161 | select:disabled, input:disabled { opacity: 0.45; cursor: default; } | |
7cc177f6 2026-09-07 Pavel Safronov | 162 | button:disabled:hover { background: #1b2130; } |
09a64747 2026-09-07 Pavel Safronov | 163 | button.active { border-color: var(--accent); color: var(--accent); background: #16233a; } |
| 164 | ||
| 165 | .list { list-style: none; margin: 0; padding: 0; max-height: 260px; overflow-y: auto; } | |
| 166 | .list li { display: flex; align-items: center; gap: 8px; padding: 5px 6px; | |
| 167 | border-radius: 5px; cursor: pointer; } | |
| 168 | .list li:hover { background: #1b2130; } | |
| 169 | .list li.sel { background: #1d2942; } | |
| 170 | .list .swatch { width: 9px; height: 9px; border-radius: 50%; flex: none; } | |
| 171 | .list .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| 172 | .list .kd { color: var(--dim); font-size: 10px; } | |
| 173 | ||
| 174 | #detail p { margin: 0 0 8px; } | |
| 175 | #d-name { font-size: 14px; font-weight: 600; } | |
| 176 | label { display: block; margin-bottom: 9px; color: var(--dim); font-size: 11px; } | |
| 177 | label.row { display: flex; align-items: center; gap: 7px; color: var(--fg); font-size: 12px; } | |
9d5a061c 2026-09-27 Pavel Safronov | 178 | input[type=number], select { |
09a64747 2026-09-07 Pavel Safronov | 179 | width: 100%; margin-top: 3px; padding: 6px 8px; border-radius: 5px; |
| 180 | border: 1px solid var(--line); background: #0e1219; color: var(--fg); | |
9d5a061c 2026-09-27 Pavel Safronov | 181 | font: inherit; font-size: 12px; |
09a64747 2026-09-07 Pavel Safronov | 182 | } |
| 183 | input[type=range] { width: 100%; margin-top: 6px; accent-color: var(--accent); } | |
| 184 | .warn { color: #e8c46a; } | |
| 185 | .links { display: flex; flex-wrap: wrap; gap: 4px 12px; margin: 0 0 14px; font-size: 11px; } | |
| 186 | .links a { color: var(--accent); text-decoration: none; } | |
| 187 | .links a:hover { text-decoration: underline; } | |
a088e5d5 2026-09-17 Pavel Safronov | 188 | .board { list-style: none; margin: 0 0 10px; padding: 0; } |
| 189 | .board li { padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12px; } | |
| 190 | .board li.now { color: var(--accent); } | |
| 191 | .board .rev { color: var(--dim); font-size: 11px; margin-top: 2px; } | |
| 192 | .board s { color: var(--dim); } | |
fc11b064 2026-09-08 Pavel Safronov | 193 | |
| 194 | /** | |
| 195 | * Narrow viewports: the sidebar becomes a drawer. | |
| 196 | * | |
| 197 | * The desktop layout is a fixed 340 px panel beside the world. On a 390 px phone | |
| 198 | * that leaves the world about fifty pixels — the 3D views rendered into a black | |
| 199 | * slot and there was no way to get the panel out of the way. Below 720 px the | |
| 200 | * panel comes out of the grid entirely and slides in over the world instead, so | |
| 201 | * the world always has the whole screen and the panel is one tap away. | |
| 202 | */ | |
| 203 | #panel-toggle, #panel-close, .scrim { display: none; } | |
| 204 | ||
| 205 | @media (max-width: 720px) { | |
| 206 | #app { grid-template-columns: 1fr; } | |
| 207 | ||
| 208 | aside { | |
| 209 | position: fixed; top: 0; right: 0; bottom: 0; z-index: 20; | |
| 210 | /* Never the full width: the strip of world left showing is what tells you | |
| 211 | the panel is a drawer over the map and not a page you navigated to. */ | |
| 212 | width: min(340px, 86vw); | |
| 213 | transform: translateX(100%); | |
| 214 | transition: transform 0.18s ease; | |
| 215 | box-shadow: -12px 0 28px rgba(0, 0, 0, 0.5); | |
| 216 | } | |
| 217 | #app.panel-open aside { transform: translateX(0); } | |
| 218 | ||
| 219 | /* Opens the drawer. Sits opposite the view switcher, and goes away once the | |
| 220 | drawer is open — the Close button in the panel header takes over there, in | |
| 221 | normal flow, so it cannot end up floating over scrolled content. */ | |
| 222 | #panel-toggle { | |
| 223 | display: block; position: fixed; top: 12px; right: 12px; z-index: 21; | |
| 224 | width: auto; margin: 0; padding: 6px 14px; font-size: 12px; | |
| 225 | background: rgba(10, 13, 20, 0.86); | |
| 226 | } | |
| 227 | #app.panel-open #panel-toggle { display: none; } | |
| 228 | #panel-close { display: block; width: auto; margin: 0 0 0 auto; padding: 4px 12px; } | |
b4b8e46c 2026-09-28 Pavel Safronov | 229 | /* With the picker there, it takes the push to the right and Close sits beside it. */ |
| 230 | #branch:not([hidden]) + #panel-close { margin-left: 0; } | |
fc11b064 2026-09-08 Pavel Safronov | 231 | |
| 232 | /* That strip of world beside the drawer is still live canvas, and in writer | |
| 233 | mode a stray tap on it drops a marker. The scrim eats those and closes. */ | |
| 234 | #app.panel-open .scrim { | |
| 235 | display: block; position: fixed; inset: 0; z-index: 19; | |
| 236 | background: rgba(5, 7, 13, 0.55); | |
| 237 | } | |
| 238 | ||
| 239 | /* The spin readout shares the top-right corner with the toggle. */ | |
| 240 | #spin { top: 54px; } | |
| 241 | ||
| 242 | /* The standing readout is ~65 monospace characters wide, which overruns a | |
| 243 | 390 px screen at 11 px. Shrink the type rather than wrap it: these are | |
| 244 | aligned columns, and wrapping them destroys the thing they are for. */ | |
| 245 | .overlay { font-size: 9px; padding: 6px 8px; } | |
3e898b51 2026-09-28 Pavel Safronov | 246 | /* Clear of the gauge; its longest lines were already past the screen's edge. */ |
| 247 | .overlay.collapsible { padding: 0; max-width: calc(100% - 60px); overflow: hidden; } | |
| 248 | ||
| 249 | /* No room for the landmark names beside the gauge on a phone: ticks only, and the | |
| 250 | readout by the thumb still says which one you are near. */ | |
| 251 | .gauge { top: 96px; right: 14px; } | |
| 252 | .gauge-mark span, .gauge-mark i { display: none; } | |
7a5edd5f 2026-10-02 Pavel Safronov | 253 | /* The readout box is a big share of a phone's view, so it fades once the height has been |
| 254 | still for two seconds, and comes back the moment it changes or the gauge is touched | |
| 255 | (eye-gauge.ts wake). The thumb stays where it is: the height is still on the scale. */ | |
| 256 | .gauge-readout { opacity: 0; transition: opacity 0.6s ease; } | |
| 257 | .gauge-readout.awake { opacity: 1; transition: opacity 0.15s ease; } | |
fc11b064 2026-09-08 Pavel Safronov | 258 | } |
a088e5d5 2026-09-17 Pavel Safronov | 259 | |
| 260 | /* The scenic tour (interior.ts): a black fade between scenes, and the scene's title. */ | |
| 261 | .tour-fade { position: absolute; inset: 0; background: #000; pointer-events: none; opacity: 0; } | |
| 262 | .tour-caption { | |
| 263 | position: absolute; top: 12px; left: 50%; transform: translateX(-50%); | |
| 264 | max-width: min(640px, calc(100% - 32px)); text-align: center; white-space: pre-line; | |
| 265 | background: rgba(10,13,20,0.72); border: 1px solid var(--line); border-radius: 6px; | |
| 266 | padding: 8px 14px; font-size: 13px; line-height: 1.5; pointer-events: none; | |
| 267 | } | |
ec1a6075 2026-09-30 Pavel Safronov | 268 | /* The leg's progress: the caption's border filling in clockwise until the next leg (D-094). */ |
| 269 | .tour-ring { | |
| 270 | position: absolute; inset: -1px; width: calc(100% + 2px); height: calc(100% + 2px); | |
| 271 | overflow: visible; pointer-events: none; | |
| 272 | } | |
| 273 | .tour-ring rect { | |
| 274 | x: 1px; y: 1px; width: calc(100% - 2px); height: calc(100% - 2px); | |
| 275 | fill: none; stroke: var(--accent); stroke-width: 2px; stroke-opacity: 0.85; | |
| 276 | stroke-dasharray: 0 10000; stroke-linecap: round; | |
| 277 | } | |
a088e5d5 2026-09-17 Pavel Safronov | 278 | @media (max-width: 720px) { .tour-caption { top: 56px; font-size: 11px; } } |