engine/scrollcraft.css
engine/scrollcraft.cssBrowse 22 files
5,806 tokens
19,769 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1/* ============================================================================2 scrollcraft.css: the taste floor3 ----------------------------------------------------------------------------4 Two layers, and the split matters:5 6 TOKENS what you override per brand. Colour roles, the type ramp, the7 spacing scale, elevation, motion. A brand is ~12 values.8 DEVICES what the engine drives. Do not restyle these to taste; they are9 the mechanism. Style your own markup instead.10 11 Nothing here draws a "component". There are no card, badge, or pill classes,12 because a stylesheet that ships those is how every page built on it ends up13 with the same shapes. You get a floor and a vocabulary; the composition is14 yours.15 ========================================================================== */16 17/* ---------------------------------------------------------------- tokens -- */18:root {19 /* --- colour roles. Override these six and the page is rebranded. -------- */20 --sc-canvas: #08090b; /* the page ground. Drift interpolates this. */21 --sc-surface: #101217; /* anything raised off the ground */22 --sc-ink: #f4f2ef; /* primary text */23 --sc-ink-soft: #9a9ba1; /* secondary text. Tinted, never flat gray. */24 --sc-accent: #d8ff3e; /* ONE accent. It owns a region, not confetti. */25 --sc-accent-ink: #08090b; /* text that sits on the accent */26 27 --sc-hairline: color-mix(in oklab, var(--sc-ink) 12%, transparent);28 --sc-hairline-strong: color-mix(in oklab, var(--sc-ink) 22%, transparent);29 30 /* --- type. Two families max. Display carries voice, text carries prose. - */31 --sc-font-display: "Instrument Sans", "Geist", system-ui, sans-serif;32 --sc-font-text: "Geist", system-ui, sans-serif;33 --sc-font-mono: "Geist Mono", ui-monospace, monospace;34 35 /* Fluid ramp. Tracking tightens as size grows because a face set at 6rem36 with 0 tracking reads loose; this is optical correction, not decoration. */37 --sc-t-xs: clamp(0.75rem, 0.72rem + 0.15vw, 0.82rem);38 --sc-t-sm: clamp(0.875rem, 0.84rem + 0.18vw, 0.95rem);39 --sc-t-base: clamp(1rem, 0.96rem + 0.22vw, 1.125rem);40 --sc-t-lg: clamp(1.2rem, 1.1rem + 0.5vw, 1.5rem);41 --sc-t-xl: clamp(1.6rem, 1.35rem + 1.2vw, 2.25rem);42 --sc-t-2xl: clamp(2.1rem, 1.6rem + 2.4vw, 3.4rem);43 --sc-t-3xl: clamp(2.8rem, 1.9rem + 4.2vw, 5rem);44 --sc-t-4xl: clamp(3.4rem, 1.9rem + 6.6vw, 7.5rem);45 46 --sc-track-tight: -0.035em;47 --sc-track-snug: -0.02em;48 --sc-track-normal: -0.005em;49 --sc-track-wide: 0.08em;50 51 --sc-leading-none: 0.94;52 --sc-leading-tight: 1.06;53 --sc-leading-body: 1.62;54 55 --sc-measure: 62ch;56 57 /* --- space. 4px base: the useful middle steps an 8-only scale misses. --- */58 --sc-1: 0.25rem; --sc-2: 0.5rem; --sc-3: 0.75rem; --sc-4: 1rem;59 --sc-5: 1.5rem; --sc-6: 2rem; --sc-7: 3rem; --sc-8: 4rem;60 --sc-9: 6rem; --sc-10: 8rem; --sc-11: 12rem;61 62 /* Section rhythm is fluid so a phone doesn't inherit desktop air. */63 --sc-section: clamp(4.5rem, 9vw, 11rem);64 --sc-gutter: clamp(1.25rem, 5vw, 5.5rem);65 --sc-maxw: 82rem;66 67 /* --- radius. Pick one scale for the page and hold it. ------------------- */68 --sc-r-sm: 6px; --sc-r-md: 12px; --sc-r-lg: 20px; --sc-r-pill: 999px;69 70 /* --- elevation. Every shadow has an offset AND a soft blur, and is tinted71 to the canvas hue. A zero-offset coloured halo is decoration, not72 depth, so there isn't one here. ---------------------------------- */73 --sc-shadow-color: 220 40% 2%;74 --sc-e1: 0 1px 2px hsl(var(--sc-shadow-color) / 0.28),75 0 2px 6px -1px hsl(var(--sc-shadow-color) / 0.20);76 --sc-e2: 0 2px 4px hsl(var(--sc-shadow-color) / 0.24),77 0 8px 18px -4px hsl(var(--sc-shadow-color) / 0.30);78 --sc-e3: 0 4px 8px hsl(var(--sc-shadow-color) / 0.22),79 0 18px 40px -8px hsl(var(--sc-shadow-color) / 0.38),80 0 48px 90px -24px hsl(var(--sc-shadow-color) / 0.30);81 82 /* Edge light: a 1px top highlight sells a raised surface better than any83 amount of blur, because real raised things catch light on their lip. */84 --sc-edge: inset 0 1px 0 color-mix(in oklab, var(--sc-ink) 10%, transparent);85 86 /* --- motion. Built-in CSS easings are too weak for UI. ------------------ */87 --sc-ease-out: cubic-bezier(0.23, 1, 0.32, 1);88 --sc-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);89 --sc-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);90 --sc-d-fast: 160ms; --sc-d-base: 240ms; --sc-d-slow: 420ms;91 92 --sc-z-stage: 1; --sc-z-copy: 20; --sc-z-chrome: 60;93}94 95/* --------------------------------------------------------------- reset -- */96*, *::before, *::after { box-sizing: border-box; }97html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }98@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }99 100body {101 margin: 0;102 background: var(--sc-canvas);103 color: var(--sc-ink);104 font-family: var(--sc-font-text);105 font-size: var(--sc-t-base);106 line-height: var(--sc-leading-body);107 letter-spacing: var(--sc-track-normal);108 -webkit-font-smoothing: antialiased;109 text-rendering: optimizeLegibility;110 font-synthesis-weight: none;111 overflow-x: clip;112}113img, video, canvas, svg { display: block; max-width: 100%; }114button, input, select, textarea { font: inherit; color: inherit; }115 116/* ------------------------------------------------- the browser surfaces --117 The parts you did not draw still carry the design. Selection, caret, focus118 ring, scrollbar and tabular numerals all ship with defaults that belong to no119 design system. Theming them is the cheapest signal that a page was built120 rather than assembled, and it is the step that gets skipped most reliably. */121::selection { background: var(--sc-accent); color: var(--sc-accent-ink); }122:root { caret-color: var(--sc-accent); accent-color: var(--sc-accent); }123:focus-visible {124 outline: 2px solid var(--sc-accent);125 outline-offset: 3px;126 border-radius: var(--sc-r-sm);127}128:where(a) { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.22em; }129@supports (scrollbar-color: auto) {130 html { scrollbar-color: color-mix(in oklab, var(--sc-ink) 26%, transparent) transparent; scrollbar-width: thin; }131}132:where(table, .sc-nums, [data-sc-count]) { font-variant-numeric: tabular-nums; }133 134/* ----------------------------------------------------------- typography -- */135.sc-display {136 font-family: var(--sc-font-display);137 font-weight: 500;138 line-height: var(--sc-leading-none);139 letter-spacing: var(--sc-track-tight);140 text-wrap: balance;141 margin: 0;142}143.sc-display--xl { font-size: var(--sc-t-4xl); }144.sc-display--lg { font-size: var(--sc-t-3xl); }145.sc-display--md { font-size: var(--sc-t-2xl); letter-spacing: var(--sc-track-snug); }146.sc-lede {147 font-size: var(--sc-t-lg);148 line-height: 1.44;149 letter-spacing: var(--sc-track-snug);150 color: var(--sc-ink);151 max-width: 46ch;152 text-wrap: pretty;153 margin: 0;154}155.sc-body { max-width: var(--sc-measure); color: var(--sc-ink-soft); text-wrap: pretty; margin: 0; }156.sc-label {157 font-family: var(--sc-font-mono);158 font-size: var(--sc-t-xs);159 letter-spacing: var(--sc-track-wide);160 text-transform: uppercase;161 color: var(--sc-ink-soft);162}163 164/* ---------------------------------------------------------------- layout -- */165.sc-wrap { width: 100%; max-width: var(--sc-maxw); margin-inline: auto; padding-inline: var(--sc-gutter); }166.sc-section { padding-block: var(--sc-section); }167/* More space above a heading than below it: the gap belongs to the boundary,168 not to the pair. */169.sc-stack > * + * { margin-top: var(--sc-4); }170.sc-stack > :is(h1,h2,h3) { margin-top: var(--sc-7); }171.sc-stack > :is(h1,h2,h3):first-child { margin-top: 0; }172.sc-rule { height: 1px; border: 0; background: var(--sc-hairline); margin: 0; }173 174/* =============================================================== DEVICES ==175 Driven by the engine. Restyle your own markup, not these. */176 177.sc-act--pinned { position: relative; }178.sc-stage {179 position: sticky; top: 0;180 height: 100vh; height: 100svh;181 overflow: clip;182 z-index: var(--sc-z-stage);183}184 185/* scrub / sequence media */186.sc-stage :is(video[data-sc-scrub], canvas[data-sc-sequence], .sc-stage__poster) {187 position: absolute; inset: 0;188 width: 100%; height: 100%;189 object-fit: cover;190}191video[data-sc-scrub] { opacity: 0; transition: opacity 380ms var(--sc-ease-out); }192.sc-has-clip video[data-sc-scrub] { opacity: 1; }193/* The poster is a live frame-holder, not a placeholder: it stays up until a194 real video frame has painted, which is what stops the blank-stage flash. */195.sc-stage__poster { z-index: 0; transition: opacity 380ms var(--sc-ease-out); }196.sc-has-clip .sc-stage__poster { opacity: 0; }197 198/* A scrim only where text sits. A full-frame darkening layer flattens the199 image everywhere to fix contrast in one corner. */200.sc-scrim {201 position: absolute; inset: 0; pointer-events: none; z-index: 1;202 background: linear-gradient(203 var(--sc-scrim-angle, 180deg),204 color-mix(in oklab, var(--sc-canvas) var(--sc-scrim-a, 78%), transparent) 0%,205 color-mix(in oklab, var(--sc-canvas) 30%, transparent) 42%,206 transparent 68%);207}208/* Point the scrim at the copy. A scrim gradient that darkens the top while the209 copy sits at the bottom fixes contrast nowhere and flattens the image210 everywhere, which is the most common way text-over-video goes wrong. */211.sc-scrim--bottom { --sc-scrim-angle: 0deg; }212.sc-scrim--left { --sc-scrim-angle: 90deg; }213.sc-scrim--right { --sc-scrim-angle: 270deg; }214 215/* Corner scrims, for copy anchored to a corner. An edge gradient has to darken216 a whole band across the frame to cover one corner; a corner gradient puts the217 density where the text is and leaves the rest of the image alone. Pair with218 .sc-copy--lead / .sc-copy--trail. */219.sc-scrim--lead {220 background: linear-gradient(to top right,221 color-mix(in oklab, var(--sc-canvas) 94%, transparent) 0%,222 color-mix(in oklab, var(--sc-canvas) 72%, transparent) 30%,223 color-mix(in oklab, var(--sc-canvas) 30%, transparent) 52%,224 transparent 72%);225}226.sc-scrim--trail {227 background: linear-gradient(to top left,228 color-mix(in oklab, var(--sc-canvas) 94%, transparent) 0%,229 color-mix(in oklab, var(--sc-canvas) 72%, transparent) 30%,230 color-mix(in oklab, var(--sc-canvas) 30%, transparent) 52%,231 transparent 72%);232}233/* A band across the bottom, for copy that spans the full width of the frame.234 That is what both corner anchors become below 860px, and it is also the right235 shape whenever the copy block is wider than a corner. */236.sc-scrim--band {237 background: linear-gradient(to top,238 color-mix(in oklab, var(--sc-canvas) 94%, transparent) 0%,239 color-mix(in oklab, var(--sc-canvas) 74%, transparent) 22%,240 color-mix(in oklab, var(--sc-canvas) 30%, transparent) 42%,241 transparent 58%);242}243.sc-scrim--vignette {244 background: radial-gradient(120% 90% at 50% 45%, transparent 40%,245 color-mix(in oklab, var(--sc-canvas) 70%, transparent) 100%);246}247 248/* copy over a stage.249 max-width is in rem, never ch. A `ch` on this container resolves against the250 CONTAINER's font-size (body size), not the display size of the heading inside251 it, so `max-width: 20ch` here silently produces a ~180px column and wraps a252 hero headline to six lines. Set a ch measure on the text element itself. */253.sc-copy {254 position: absolute; z-index: var(--sc-z-copy);255 inset-inline: var(--sc-gutter);256 max-width: min(46rem, 76vw);257 will-change: opacity, transform;258}259.sc-copy--lead { left: var(--sc-gutter); bottom: clamp(3rem, 12vh, 9rem); }260/* inset-inline FIRST. It is the shorthand for left+right, so declaring it after261 `left: 50%` resets left to auto and the block drifts off the left edge. */262.sc-copy--center { inset-inline: auto; left: 50%; top: 50%; translate: -50% -50%; text-align: center; }263.sc-copy--trail { inset-inline: auto; right: var(--sc-gutter); bottom: clamp(3rem, 12vh, 9rem); text-align: right; }264 265/* cue defaults: the engine writes opacity/transform inline, these are the266 pre-paint state so nothing flashes before the first read() */267[data-sc-cue] { opacity: 0; will-change: opacity, transform; }268.sc-ready [data-sc-cue] { transition: none; }269 270/* kinetic split */271.sc-split { display: inline-block; overflow: hidden; vertical-align: top; }272/* Descenders live below the baseline; a line mask clipped to the line box eats273 the tails off g, y, p, j. The padding buys them room back. */274.sc-split--line { display: block; padding-bottom: 0.14em; margin-bottom: -0.14em; }275.sc-split__i { display: inline-block; will-change: transform, opacity; }276.sc-is-split { opacity: 1 !important; }277 278/* horizontal rail */279[data-sc-pan] { display: flex; will-change: transform; }280 281/* reveal + parallax */282[data-sc-reveal] { will-change: clip-path; }283[data-sc-parallax] { will-change: transform; }284 285/* flow reveal (fires once) */286[data-sc-in], [data-sc-stagger] > * {287 opacity: 0;288 transform: translate3d(0, 14px, 0);289 transition: opacity 620ms var(--sc-ease-out), transform 620ms var(--sc-ease-out);290}291[data-sc-in].sc-in, [data-sc-stagger] > .sc-in { opacity: 1; transform: none; }292 293/* pointer devices */294[data-sc-tilt] { transform-style: preserve-3d; will-change: transform; }295[data-sc-magnet] { will-change: transform; }296/* isolation always; position only if nothing more specific already set it.297 Written with :where() (zero specificity) on purpose: a plain `position:298 relative` here has the same weight as `.sc-stage` and, being later in the299 file, silently wins. A stage carrying both attributes then stops being300 sticky and the whole act scrolls away instead of pinning. */301[data-sc-spotlight] { isolation: isolate; }302:where([data-sc-spotlight]) { position: relative; }303[data-sc-spotlight]::after {304 content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 2;305 background: radial-gradient(306 28rem 28rem at calc(var(--sc-mx, 0.5) * 100%) calc(var(--sc-my, 0.5) * 100%),307 color-mix(in oklab, var(--sc-accent) 16%, transparent), transparent 70%);308 opacity: 0; transition: opacity var(--sc-d-slow) var(--sc-ease-out);309}310[data-sc-spotlight]:hover::after { opacity: 1; }311 312/* =========================================================== WORLDFLIGHT ==313 One fixed stage for the whole page. The only thing in document flow is the314 spacer, and it is empty. If you find yourself adding a section here, you want315 act mode instead: the moment a real block scrolls past the fixed stage, the316 page has a seam again and the whole point is gone. */317 318[data-sc-mode="worldflight"] { position: relative; }319 320.sc-world {321 position: fixed; inset: 0;322 overflow: clip;323 z-index: var(--sc-z-stage);324 background: var(--sc-canvas);325}326 327/* Every leg is mounted for the life of the page and stacked in the same box.328 The engine owns opacity, visibility and z-index here; do not set them. */329.sc-world__seg {330 position: absolute; inset: 0;331 opacity: 0;332 will-change: opacity;333}334.sc-world__seg > :is(video, img, canvas) {335 position: absolute; inset: 0;336 width: 100%; height: 100%;337 object-fit: cover;338}339/* The poster is the frame-holder AND the first camera move: the engine pushes340 it in slowly until a real decoded frame is available to replace it. */341.sc-world__poster {342 z-index: 0;343 transform-origin: 50% 50%;344 transition: opacity 420ms var(--sc-ease-out);345}346.sc-world__seg > video { z-index: 1; }347.sc-world__seg.sc-has-clip .sc-world__poster { opacity: 0; }348/* The act stylesheet lights a clip through its ancestor act. A worldflight leg349 has no act, so match the class the engine also writes on the clip itself. */350video[data-sc-scrub].sc-has-clip { opacity: 1; }351 352/* The copy layer is fixed too, and inert by default: the engine hands353 pointer-events back to a block only while it is actually legible. */354.sc-world__copy {355 position: fixed; inset: 0;356 z-index: var(--sc-z-copy);357 pointer-events: none;358}359.sc-world__scrim { position: absolute; inset: 0; pointer-events: none; z-index: 0; }360[data-sc-copy] {361 opacity: 0;362 pointer-events: none;363 will-change: opacity, transform;364}365.sc-ready [data-sc-copy] { transition: none; }366 367/* The track. Empty on purpose. */368.sc-world__spacer { pointer-events: none; }369 370/* scroll progress */371[data-sc-progress] {372 position: fixed; inset: 0 0 auto 0; height: 2px; z-index: var(--sc-z-chrome);373 background: var(--sc-accent); transform-origin: 0 50%; transform: scaleX(0);374 will-change: transform;375}376 377/* ----------------------------------------------------------- atmosphere --378 Depth is not only shadow. Grain keeps a flat dark ground from banding, and379 it is the difference between "a dark page" and "a lit room". */380.sc-grain { position: fixed; inset: 0; pointer-events: none; z-index: 3; opacity: 0.045; }381.sc-grain::before {382 content: ""; position: absolute; inset: -50%;383 background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/></filter><rect width='140' height='140' filter='url(%23n)'/></svg>");384}385 386/* -------------------------------------------------------- reduced motion -- */387@media (prefers-reduced-motion: reduce) {388 /* Fewer and gentler, not zero. Opacity still carries the reveal so the page389 is comprehensible; every position change is dropped. */390 [data-sc-in], [data-sc-stagger] > * { transform: none; transition-duration: 220ms; }391 [data-sc-parallax], [data-sc-pan], .sc-split__i { transform: none !important; }392 /* A wipe is a position change too. Show reveal content settled. */393 [data-sc-reveal] { clip-path: none !important; }394 /* A rail's transform IS its navigation, not decoration. Zeroing it the way395 parallax and kinetic type are zeroed parks the act on its first screenful396 and makes everything past the fold unreachable: a device degrading into397 missing content. Hand the travel back to the reader as an ordinary scroll398 region instead, so the same items are all still gettable without motion.399 A build that would rather re-lay the rail out as a grid can still do that;400 this is the floor, not the ceiling. */401 [data-sc-act="pan"] .sc-stage,402 [data-sc-act="pan"] [data-sc-stage] {403 overflow-x: auto; overflow-y: hidden;404 overscroll-behavior-x: contain;405 scroll-snap-type: x proximity;406 }407 [data-sc-pan] > * { scroll-snap-align: center; }408 [data-sc-spotlight]::after { display: none; }409 /* A worldflight keeps its whole story here. No clip is ever fetched, so the410 posters are the film, and they cross-dissolve through exactly the same411 seams at exactly the same scroll positions. The push-in and the copy drift412 are the only things that go. */413 .sc-world__poster, [data-sc-copy] { transform: none !important; }414}415 416/* --------------------------------------------------------------- mobile -- */417@media (max-width: 860px) {418 .sc-copy { inset-inline: var(--sc-gutter); max-width: none; }419 .sc-copy--trail { text-align: left; }420 .sc-stage { height: 100svh; }421 /* .sc-copy--trail re-anchors to the left and spans the full width here, so a422 bottom-RIGHT corner gradient darkens the one corner the copy just left and423 guarantees a contrast failure over any bright clip. Point the density at424 the copy: the same band .sc-scrim--band paints. */425 .sc-scrim--trail {426 background: linear-gradient(to top,427 color-mix(in oklab, var(--sc-canvas) 94%, transparent) 0%,428 color-mix(in oklab, var(--sc-canvas) 74%, transparent) 22%,429 color-mix(in oklab, var(--sc-canvas) 30%, transparent) 42%,430 transparent 58%);431 }432}433