chrome.css (15535B)
1 /* ============================================================================ 2 Chrome — the site's form language, ported from the main DÆMON build. 3 4 This file replaces the old glass.css. Where that file made a container 5 out of translucency (blur, tint, inner highlight, 14px radius), this one 6 makes one out of a hairline and a value change. The objects below are 7 the whole vocabulary; every page is assembled from them. 8 9 .slab flat panel — 1px rule, square, no shadow 10 .plate inverse (night) panel, defined in tokens.css 11 .grain the film over everything 12 .micro 10px mono label — the site's furniture type 13 .eyebrow `03 ^: LABEL` lockup with a keyed rule 14 .btn flat control 15 .wipe ink wipe for index rows 16 .clip / .stretch the lockup's scale rig 17 .corners cyberpunk corner brackets 18 .segbar / .seg / .seg--accent tmux-style segmented status bar 19 .glitch RGB-split wordmark glitch, keyed off `data-text` 20 .scanlines CRT scanline overlay for dark plates 21 ========================================================================== */ 22 23 /* ---- The film ------------------------------------------------------------ */ 24 /* Fractal noise laid over the page so a flat colour reads as stock rather 25 than as a swatch. It is the one thing standing in for the texture the 26 glass build got from blur, and it is why removing the blur does not 27 leave the page feeling like unpainted CSS. 28 29 Multiply on paper, screen on ink (tokens.css) — noise has to be laid 30 *into* a light ground and *added* to a dark one; the same blend on both 31 is invisible on one of them. */ 32 .grain { 33 position: fixed; 34 inset: 0; 35 z-index: 1; 36 pointer-events: none; 37 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E"); 38 opacity: var(--grain-opacity); 39 mix-blend-mode: var(--grain-blend); 40 } 41 42 /* A plate carries its own grain, because it pins the night palette for its 43 subtree and therefore needs the dark blend while the page around it is 44 still on the light one. */ 45 .grain--local { 46 position: absolute; 47 z-index: 2; 48 } 49 50 /* ---- Panels -------------------------------------------------------------- */ 51 .slab { 52 border: 1px solid var(--rule); 53 background: var(--surface); 54 } 55 /* The heavier variant, for the one panel on a screen that is the point of 56 it — the site draws these at full ink rather than at rule weight. */ 57 .slab--ink { 58 border-color: var(--fg); 59 background: var(--base); 60 } 61 62 /* ---- Type furniture ------------------------------------------------------ */ 63 /* 10px mono, uppercase, widely tracked. Every eyebrow, kind mark, stat 64 label, tab and caption on the site is this object. */ 65 .micro { 66 font-family: var(--font-mono); 67 font-size: var(--step-micro); 68 letter-spacing: var(--track-micro); 69 text-transform: uppercase; 70 line-height: 1.6; 71 } 72 .micro--wide { letter-spacing: var(--track-wide); } 73 74 /* The section lockup: an index, the site's self-naming `^:` mark, a label, 75 then a rule that runs out to the end of the line in the section's own 76 hue. It is what tells you which of the twelve domains you are in 77 without a heading having to say it twice. */ 78 .eyebrow { 79 display: flex; 80 align-items: center; 81 gap: 0.9rem; 82 font-family: var(--font-mono); 83 font-size: var(--step-micro); 84 letter-spacing: var(--track-wide); 85 text-transform: uppercase; 86 color: var(--fg-faint); 87 } 88 .eyebrow__n { color: var(--acc, var(--accent)); } 89 .eyebrow__mark { color: var(--fg-faint); opacity: 0.7; } 90 .eyebrow__rule { 91 flex: 1; 92 height: 1px; 93 background: var(--acc, var(--accent)); 94 opacity: 0.5; 95 } 96 97 /* Section heading — Archivo at its heaviest, condensed, tracked in tight. 98 Uppercase is load-bearing: at this weight and width the lowercase 99 letterforms lose their counters and the word turns into a block. */ 100 .display { 101 font-family: var(--font-display); 102 font-weight: 700; 103 font-stretch: 84%; 104 letter-spacing: -0.03em; 105 line-height: 0.94; 106 text-transform: uppercase; 107 } 108 109 /* ---- Controls ------------------------------------------------------------ */ 110 .btn { 111 display: inline-flex; 112 align-items: center; 113 gap: 0.55rem; 114 padding: 0.72rem 1.1rem; 115 font-family: var(--font-mono); 116 font-size: 11px; 117 letter-spacing: var(--track-micro); 118 text-transform: uppercase; 119 /* The label is a single tracked line. Without this it wraps at the 120 button's intrinsic width and the control turns into a three-line 121 block, which at this letter-spacing is unreadable. */ 122 white-space: nowrap; 123 border: 1px solid transparent; 124 background: var(--fg); 125 color: var(--base); 126 transition: background-color 240ms ease, color 200ms ease, border-color 220ms ease; 127 } 128 /* An inline SVG with no intrinsic size resolves to 300×150 and detonates 129 the control. Every icon in a button is a 14px mark beside the label. */ 130 .btn svg { width: 14px; height: 14px; flex: none; } 131 .btn:hover { background: var(--accent-2); } 132 .btn--ghost { 133 background: transparent; 134 color: var(--fg); 135 border-color: var(--rule-hi); 136 } 137 .btn--ghost:hover { background: var(--fg); color: var(--base); border-color: var(--fg); } 138 139 /* A chip is a bordered label, never a filled pill. Square, hairline, hue 140 from whatever it qualifies. */ 141 .chip { 142 display: inline-flex; 143 align-items: center; 144 gap: 0.5rem; 145 padding: 0.36rem 0.7rem; 146 font-family: var(--font-mono); 147 font-size: var(--step-micro); 148 letter-spacing: var(--track-micro); 149 text-transform: uppercase; 150 border: 1px solid color-mix(in oklab, var(--acc, var(--accent)) 55%, transparent); 151 color: var(--acc, var(--accent)); 152 transition: background-color 240ms ease, color 200ms ease; 153 } 154 a.chip:hover { background: var(--acc, var(--accent)); color: var(--base); } 155 156 /* The live dot — a filled core with a ring expanding off it. Used for 157 anything the page wants to claim is current rather than static. */ 158 .dot { 159 position: relative; 160 display: block; 161 width: 6px; 162 height: 6px; 163 flex: none; 164 } 165 .dot i, 166 .dot u { 167 position: absolute; 168 inset: 0; 169 border-radius: 50%; 170 display: block; 171 } 172 .dot i { background: currentColor; } 173 .dot u { border: 1px solid currentColor; } 174 @media (prefers-reduced-motion: no-preference) { 175 .dot i { animation: dmn-pulse 2.4s ease-in-out infinite; } 176 .dot u { animation: dmn-ring 4.2s ease-out infinite; } 177 } 178 179 /* ---- The index row's ink wipe -------------------------------------------- */ 180 /* Hovering a row fills it with ink from the left. Driven by 181 `background-position` over a hard-stop gradient rather than by 182 `background-color`, and the transition list deliberately has no `color` 183 in it — dragging a pointer down a fifty-row index is the one interaction 184 here that can drop frames, and a colour transition on every row it 185 crosses is what does it. The text colour flips instantly instead. */ 186 .wipe { 187 background-image: linear-gradient(90deg, var(--fg) 0 50%, transparent 50% 100%); 188 background-size: 200% 100%; 189 background-position: 100% 0; 190 background-repeat: no-repeat; 191 transition: background-position 380ms var(--ease-in), color 1ms; 192 } 193 .wipe:hover { background-position: 0 0; } 194 195 /* ---- Corner brackets ----------------------------------------------------- */ 196 /* The cyberdeck frame: four ~10px L-ticks sitting just outside the element's 197 hairline, drawn by the two pseudo-elements — ::before carries the left 198 pair, ::after the right, each tick a 10px hard-stop gradient tile. The 199 colour is `--bracket` when set, else the theme's love; on a `.plate` the 200 pinned night palette resolves both to their night values, so the brackets 201 stay visible on dark grounds without a special case. 202 203 One caveat: both pseudos are spent on the brackets, so an element that 204 already needs ::before/::after for something else cannot also be 205 .corners. */ 206 .corners { position: relative; } 207 .corners::before, 208 .corners::after { 209 content: ""; 210 position: absolute; 211 /* -3px: the tick straddles the corner — 3px out past the hairline, 212 7px back in over it. */ 213 inset-block: -3px; 214 width: 10px; 215 z-index: 2; 216 pointer-events: none; 217 } 218 .corners::before { 219 inset-inline-start: -3px; 220 background: 221 linear-gradient(var(--bracket, var(--love)) 0 0) left top / 10px 1px no-repeat, 222 linear-gradient(var(--bracket, var(--love)) 0 0) left top / 1px 10px no-repeat, 223 linear-gradient(var(--bracket, var(--love)) 0 0) left bottom / 10px 1px no-repeat, 224 linear-gradient(var(--bracket, var(--love)) 0 0) left bottom / 1px 10px no-repeat; 225 } 226 .corners::after { 227 inset-inline-end: -3px; 228 background: 229 linear-gradient(var(--bracket, var(--love)) 0 0) right top / 10px 1px no-repeat, 230 linear-gradient(var(--bracket, var(--love)) 0 0) right top / 1px 10px no-repeat, 231 linear-gradient(var(--bracket, var(--love)) 0 0) right bottom / 10px 1px no-repeat, 232 linear-gradient(var(--bracket, var(--love)) 0 0) right bottom / 1px 10px no-repeat; 233 } 234 /* The accent variant re-keys the ticks to iris — the site's signature hue — 235 without touching the geometry. `--bracket` inherits into the pseudos. */ 236 .corners--accent { --bracket: var(--iris); } 237 238 /* ---- Segmented status bar ------------------------------------------------ */ 239 /* tmux chrome: a ruled strip of square segments divided by hairlines. The 240 bar is a flex row; segments are JetBrains Mono at furniture size; and the 241 dividers are each segment's own inline-start border rather than a 242 separate element, so an empty bar draws nothing. `--font-term` comes from 243 tokens.css — the fallback keeps this intact if that token lands later in 244 the cascade. */ 245 .segbar { 246 display: flex; 247 align-items: stretch; 248 border-block: 1px solid var(--rule); 249 } 250 .seg { 251 display: inline-flex; 252 align-items: center; 253 gap: 0.55rem; 254 padding: 0.62rem 0.9rem; 255 font-family: var(--font-term, 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace); 256 font-size: 10.5px; 257 letter-spacing: var(--track-micro); 258 line-height: 1; 259 text-transform: uppercase; 260 white-space: nowrap; 261 color: var(--fg-dim); 262 } 263 .seg + .seg { border-inline-start: 1px solid var(--rule); } 264 /* The filled segment — the wordmark block, the active window. Accent ground, 265 base ink; the divider hairline after it still belongs to the next seg. */ 266 .seg--accent { 267 background: var(--accent); 268 color: var(--base); 269 } 270 271 /* ---- The wordmark glitch ------------------------------------------------- */ 272 /* An RGB split keyed off `data-text`: the element carries its own string in 273 the attribute, and the pseudos replay it as love/foam copies stacked on 274 the real text, each clipped to a thin horizontal slice and nudged a pixel 275 or two sideways. `glitch-shift` is a 5s loop that spends ~92% of itself 276 idle — the tear is the first 0.4s — so the mark reads clean and only 277 occasionally breaks. Under reduced motion the animation is never applied 278 and the copies sit fully clipped: the mark is completely static. */ 279 .glitch { position: relative; } 280 .glitch::before, 281 .glitch::after { 282 content: attr(data-text); 283 position: absolute; 284 inset: 0; 285 pointer-events: none; 286 user-select: none; 287 /* Idle = invisible; the burst keyframes open and reclose the slice. */ 288 clip-path: inset(50% 0 50% 0); 289 } 290 .glitch::before { color: var(--love); } 291 .glitch::after { color: var(--foam); } 292 @media (prefers-reduced-motion: no-preference) { 293 .glitch::before { animation: glitch-shift 5s linear infinite; } 294 /* Reversed and offset, so the two copies never tear on the same line. */ 295 .glitch::after { animation: glitch-shift 5s linear 0.2s infinite reverse; } 296 } 297 298 /* ---- Scanlines ----------------------------------------------------------- */ 299 /* The CRT film for dark plates: a 3px-period repeating gradient at very low 300 alpha, laid over the plate as an absolutely positioned sheet. It is its 301 own element rather than a pseudo because the plates that want it — code 302 panes, the footer, banners — already spend their pseudos on .corners. The 303 host plate supplies the positioning context; events pass straight 304 through. */ 305 .scanlines { 306 position: absolute; 307 inset: 0; 308 z-index: 1; 309 pointer-events: none; 310 background: repeating-linear-gradient(0deg, rgb(0 0 0 / 0.09) 0 1px, transparent 1px 3px); 311 } 312 /* The rolling variant drifts the film slowly downward. 120px is a whole 313 number of 3px periods, so the loop is seamless; under reduced motion the 314 animation is never applied and the film lies still. */ 315 @media (prefers-reduced-motion: no-preference) { 316 .scanlines--roll { animation: scanlines-roll 14s linear infinite; } 317 } 318 319 /* ---- Shared timing ------------------------------------------------------- */ 320 .dmn-t { 321 transition: 322 background-color 240ms ease, 323 color 200ms ease, 324 border-color 220ms ease, 325 transform 220ms var(--ease-in), 326 opacity 200ms ease; 327 } 328 329 /* ---- The lockup's scale rig ---------------------------------------------- */ 330 /* `scaleY` overflows its own line box, so a stretched display line has to 331 sit in a fixed-height wrapper that clips it — otherwise it collides with 332 whatever is under it and no amount of margin fixes it, because the 333 overflow is not in the layout. */ 334 .clip { overflow: hidden; } 335 .stretch { 336 transform-origin: 0 0; 337 white-space: nowrap; 338 user-select: none; 339 } 340 341 /* ---- Keyframes ----------------------------------------------------------- */ 342 @keyframes dmn-up { 343 from { opacity: 0; transform: translateY(18px); } 344 to { opacity: 1; transform: translateY(0); } 345 } 346 @keyframes dmn-track-in { 347 from { letter-spacing: 0.075em; transform: scaleX(0.93); } 348 to { letter-spacing: 0.012em; transform: scaleX(1); } 349 } 350 @keyframes dmn-draw { 351 from { transform: scaleX(0); } 352 to { transform: scaleX(1); } 353 } 354 @keyframes dmn-wipe-in { 355 from { clip-path: inset(0 100% 0 0); } 356 to { clip-path: inset(0 0 0 0); } 357 } 358 @keyframes dmn-pulse { 359 0%, 100% { opacity: 1; } 360 50% { opacity: 0.25; } 361 } 362 @keyframes dmn-ring { 363 0% { transform: scale(1); opacity: 0.7; } 364 70%, 100% { transform: scale(2.6); opacity: 0; } 365 } 366 @keyframes dmn-blink { 367 50% { opacity: 0; } 368 } 369 /* The glitch burst. Mostly idle by construction: every tear lives in the 370 first 8% of the 5s loop (~0.4s), then the slice closes back to the 371 invisible mid-line for the rest of the cycle. */ 372 @keyframes glitch-shift { 373 0% { clip-path: inset(8% 0 76% 0); transform: translate(-2px, -1px); } 374 1.5% { clip-path: inset(52% 0 30% 0); transform: translate(2px, 1px); } 375 3% { clip-path: inset(24% 0 58% 0); transform: translate(-2px, 0); } 376 4.5% { clip-path: inset(70% 0 12% 0); transform: translate(1px, -1px); } 377 6% { clip-path: inset(40% 0 44% 0); transform: translate(-1px, 1px); } 378 8%, 100% { clip-path: inset(50% 0 50% 0); transform: none; } 379 } 380 /* A whole number of scanline periods, so the drift loops seamlessly. */ 381 @keyframes scanlines-roll { 382 from { background-position-y: 0; } 383 to { background-position-y: 120px; } 384 } 385 386 /* Under reduced motion every entrance collapses to a single 0.01ms pass, 387 which lands on the same rest state the animation would have reached — 388 so the markup needs no second code path. */ 389 @media (prefers-reduced-motion: reduce) { 390 *, 391 *::before, 392 *::after { 393 animation-duration: 0.01ms !important; 394 animation-iteration-count: 1 !important; 395 transition-duration: 0.01ms !important; 396 scroll-behavior: auto !important; 397 } 398 }