:root { /* === TYPOGRAPHY SCALE (Augmented Fourth 1.414) === */ --font-size: 16px; --text-xs: 0.707rem; --text-sm: 0.84rem; --text-base: 1rem; --text-lg: 1.414rem; --text-xl: 2rem; --text-2xl: 2.828rem; --text-3xl: 4rem; --text-4xl: 5.657rem; --text-5xl: 8rem; /* === FONT FAMILIES (set by next/font CSS vars in layout.tsx) === */ --font-heading: var(--font-fraunces), serif; --font-body: var(--font-public-sans), sans-serif; /* === FONT WEIGHTS === */ --font-weight-heading: 900; --font-weight-body: 600; --font-weight-normal: 400; /* Fluid section title: scales from 2rem at ~267px to 8rem at ~1707px */ --text-section-title: clamp(2rem, 7.5vw, 8rem); /* === LINE HEIGHT === */ --line-height-tight: 1.2; --line-height-normal: 1.5; --line-height-relaxed: 1.65; /* === FRAUNCES VARIABLE AXES === */ --fraunces-wonk: 1; --fraunces-soft: 0; /* === COLOR PALETTE: 2-color system === */ --cream: #f4f0e8; --blue: #041cf3; /* === SEMANTIC COLORS === */ --background: var(--cream); --foreground: var(--blue); --card: var(--cream); --card-foreground: var(--blue); --primary: var(--blue); --primary-foreground: var(--cream); --secondary: var(--cream); --secondary-foreground: var(--blue); --muted: var(--cream); --muted-foreground: rgba(4, 28, 243, 0.5); --accent: var(--blue); --accent-foreground: var(--cream); --destructive: var(--blue); --border: var(--blue); --ring: var(--blue); /* === SPACING (8pt Linear Scale) === */ --space-0: 0; --space-1: 0.5rem; --space-2: 1rem; --space-3: 1.5rem; --space-4: 2rem; --space-5: 2.5rem; --space-6: 3rem; --space-7: 3.5rem; --space-8: 4rem; --space-10: 5rem; --space-12: 6rem; --space-16: 8rem; --space-20: 10rem; /* === BORDERS === */ --border-width: 3px; --radius: 0px; /* === BRUTALIST SHADOWS === */ --shadow-brutal-xs: 1px 1px 0 var(--blue); --shadow-brutal-sm: 3px 3px 0 var(--blue); --shadow-brutal: 5px 5px 0 var(--blue); --shadow-brutal-md: 7px 7px 0 var(--blue); --shadow-brutal-lg: 8px 8px 0 var(--blue); --shadow-brutal-xl: 10px 10px 0 var(--blue); --shadow-brutal-2xl: 12px 12px 0 var(--blue); /* === GRID === */ --grid-gap: var(--space-3); --section-content-width: 72rem; /* === ANIMATION === */ --ease-default: ease; --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); --ease-decelerate: cubic-bezier(0.25, 0, 0, 1); --ease-micro: cubic-bezier(0.22, 1, 0.36, 1); --duration-fast: 100ms; --duration-normal: 150ms; --duration-slow: 350ms; --duration-spring: 220ms; --delay-normal: 200ms; --slide-section-body-in: clamp(1.25rem, 5vw, 3rem); } @theme inline { --font-heading: var(--font-fraunces); --font-body: var(--font-public-sans); --color-cream: var(--cream); --color-blue: var(--blue); --color-background: var(--background); --color-foreground: var(--foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-ring: var(--ring); --color-border: var(--border); --radius-sm: var(--radius); --radius-md: var(--radius); --radius-lg: var(--radius); --container-section: var(--section-content-width); --spacing-footer: 5rem; --spacing-footer-wide: 4rem; --text-section-title: var(--text-section-title); --shadow-brutal-xs: var(--shadow-brutal-xs); --shadow-brutal-sm: var(--shadow-brutal-sm); --shadow-brutal: var(--shadow-brutal); --shadow-brutal-md: var(--shadow-brutal-md); --shadow-brutal-lg: var(--shadow-brutal-lg); --shadow-brutal-xl: var(--shadow-brutal-xl); --shadow-brutal-2xl: var(--shadow-brutal-2xl); } @layer base { * { @apply border-border; } ::selection { background-color: var(--blue); color: var(--cream); } :focus-visible { outline: var(--border-width) solid var(--blue); outline-offset: 2px; } /* Background lives on html (not body) so the grain overlay can sit behind * body's content at a negative z-index and still paint against the cream * canvas — with the background on body, a z-index:-1 body::before would hide * behind body's own background instead of showing through the page gaps. */ html { @apply bg-background; font-size: var(--font-size); scroll-behavior: smooth; /* Reserve scrollbar gutter so locking body scroll (e.g. when a modal * opens) doesn't widen the viewport and shift fixed elements. */ scrollbar-gutter: stable; } body { @apply text-foreground; font-family: var(--font-body); font-weight: var(--font-weight-body); line-height: var(--line-height-tight); overflow-x: hidden; } /* Page-wide blue dot grain. z-index -1 drops it behind body's content so * opaque surfaces (cards, badges, footer) cover it and the grain only shows * through the transparent page gaps; pointer-events: none keeps everything * clickable through it. */ body::before { @apply grain-pattern; content: ""; position: fixed; inset: 0; pointer-events: none; z-index: -1; } h1, h2, h3, h4, h5, h6 { font-family: var(--font-heading); font-weight: var(--font-weight-heading); line-height: var(--line-height-tight); font-variation-settings: "WONK" var(--fraunces-wonk), "SOFT" var(--fraunces-soft); color: var(--blue); } h1 { font-size: var(--text-4xl); } h2 { font-size: var(--text-3xl); } h3 { font-size: var(--text-2xl); } h4 { font-size: var(--text-xl); } h5 { font-size: var(--text-lg); } p { font-family: var(--font-body); font-size: var(--text-base); font-weight: var(--font-weight-body); color: var(--blue); } a { color: var(--blue); text-decoration: none; } blockquote { font-family: var(--font-heading); font-size: var(--text-xl); border-left: var(--border-width) solid var(--blue); padding-left: var(--space-4); margin: var(--space-6) 0; } } /* Button elevation transition — only transform animates; shadow snaps instantly */ .btn-transition { transition: transform 0.13s var(--ease-micro); } /* Brutalist utility classes */ .brutal-shadow { box-shadow: var(--shadow-brutal); } .brutal-shadow-sm { box-shadow: var(--shadow-brutal-sm); } .brutal-shadow-lg { box-shadow: var(--shadow-brutal-lg); } @utility brutal-border { border: var(--border-width) solid var(--blue); } @utility brutal-border-top { border-top: var(--border-width) solid var(--blue); } @utility brutal-border-bottom { border-bottom: var(--border-width) solid var(--blue); } @utility brutal-border-left { border-left: var(--border-width) solid var(--blue); } @utility brutal-border-right { border-right: var(--border-width) solid var(--blue); } /* Border drawn as an outline — painted after children, so an image's * subpixel paint bleed can't cover it. Doesn't take layout space; the * ancestor must not have overflow:hidden or the outline gets clipped. */ @utility brutal-outline { outline: var(--border-width) solid var(--blue); } /* Tiled blue dot pattern — applied to body::before (page-wide) and reusable * on any surface that should share the same paper-grain texture. The SVG * tile is rasterized once and composited cheaply via repeating background. */ @utility grain-pattern { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Ccircle cx='1' cy='1' r='1' fill='%23041cf3' opacity='0.10'/%3E%3C/svg%3E"); } /* Apply Fraunces variable axes to non-heading elements using the heading font */ .font-wonk { font-variation-settings: "WONK" var(--fraunces-wonk), "SOFT" var(--fraunces-soft); } /* Sidebar divider: bottom border on mobile, right border on desktop */ .brutal-border-sidebar { border-bottom: var(--border-width) solid var(--blue); } @media (min-width: 1024px) { .brutal-border-sidebar { border-bottom: none; border-right: var(--border-width) solid var(--blue); } } /* Editorial rich-text typography */ .rich-text { max-width: 65ch; line-height: var(--line-height-relaxed); font-feature-settings: "onum"; hanging-punctuation: first last; text-wrap: pretty; } .rich-text a { border-bottom: var(--border-width) solid var(--blue); opacity: 1; transition: opacity var(--duration-normal); } .rich-text a:hover { opacity: 0.6; } .rich-text p + p { margin-top: 1.2em; } .rich-text ul { list-style: none; padding-left: 1.5em; margin: 1em 0; } .rich-text ul li { text-indent: -1.5em; margin-top: 0.5em; } .rich-text ul li:first-child { margin-top: 0; } .rich-text ul li::before { content: "◆"; display: inline-block; width: calc(1.5em / 0.55); /* reset inherited text-indent so glyph isn't shifted inside the ::before box */ text-indent: 0; color: var(--blue); font-size: 0.55em; /* line-height matches parent so diamond centers within the first line box */ line-height: calc(var(--line-height-relaxed) / 0.55); } /* Cross-section view transition (navigation between sections) */ ::view-transition-old(section-title) { animation-name: section-fade-out; animation-duration: var(--duration-normal); animation-timing-function: var(--ease-default); animation-fill-mode: both; } ::view-transition-new(section-title) { animation-name: section-fade-in; animation-duration: var(--duration-spring); animation-timing-function: var(--ease-spring); animation-fill-mode: both; } @keyframes section-fade-out { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-8px); } } @keyframes section-fade-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } /* Disable group geometry interpolation — OLD and NEW live at different scroll * positions, so morphing the container drags the slide-in across the viewport. * Let old/new each animate at their own positions instead. */ ::view-transition-group(section-body) { animation: none; } /* Section body — snap OLD out and slide NEW in. Running an explicit OLD * animation left a visible ghost of the previous section's content sitting * behind the incoming slide; hiding it immediately keeps the transition * clean and prevents the two-content overlap. */ ::view-transition-old(section-body) { animation: none; opacity: 0; } ::view-transition-new(section-body) { animation-name: section-body-in; animation-duration: var(--duration-spring); animation-timing-function: var(--ease-spring); animation-fill-mode: both; /* Hold the start-state for this delay before the slide-in begins — gives * the snap-out a beat to register visually before new content arrives. */ animation-delay: var(--delay-normal); } @keyframes section-body-in { from { opacity: 0; transform: translateX(var(--slide-section-body-in)) scale(0.98); } to { opacity: 1; transform: translateX(0) scale(1); } } /* Page-load entry animation for the footer. Previously also carried * `view-transition-name: site-footer` to layer above section-body's slide * group, but the section-body group now has `animation: none` (purely * horizontal slide of OLD/NEW snapshots), so the footer can live in the root * snapshot during transitions — which also lets the lightbox dialog group * stack cleanly above it. */ .footer-vt { animation: footer-enter var(--duration-slow) var(--ease-spring) both; } @keyframes footer-enter { from { opacity: 0; transform: translateY(100%); } to { opacity: 1; transform: translateY(0); } } /* Lightbox dialog backdrop — flat cream wash. No filter, no gradient. * Cheapest possible; Firefox-friendly during view transitions. */ dialog.lightbox::backdrop { background-color: color-mix(in srgb, var(--cream) 80%, transparent); } /* Give the lightbox dialog its own view-transition group so it can stack * above the footer's named group (z=10) during the open/close transition. * Closed dialogs have `display: none` (UA) and don't get snapshotted, so * sharing the name across all `.lightbox` dialogs at rest is harmless — * only the open one participates in any given transition. */ dialog.lightbox { view-transition-name: lightbox-dialog; } ::view-transition-group(lightbox-dialog) { z-index: 20; } /* The image wrapper (and the thumb during the OLD snapshot of an open * transition) shares this static name — imperatively assigned only during * the morph, so there's never more than one element holding it at a time. * z=30 keeps it above the dialog (z=20) and the footer (z=10) during VT. */ ::view-transition-group(lightbox-frame) { z-index: 30; } /* Lightbox image sizing — leaves vertical headroom for the fixed close button * (sits at top-3, ~2.5rem tall). 8rem total = ~4rem top/bottom after centering, * so the button never overlaps the image. * box-sizing: content-box so max-w/max-h apply to the image pixels and the * 3px brutal-border sits outside them — avoids subpixel clipping of the * border by the dialog's overflow:hidden when both have border-box. */ @utility lightbox-image { box-sizing: content-box; max-width: calc(100vw - 2rem); max-height: calc(100vh - 10rem); } /* Disable transition animation for Firefox * since it isn't supported yet */ @supports (-moz-appearance: none) { ::view-transition-group(lightbox-frame), ::view-transition-old(lightbox-frame), ::view-transition-new(lightbox-frame) { animation-duration: 0s !important; animation-delay: 0s !important; } }