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