seedproject-web/app/src/styles.css
Carlos Arias 43ae1e2015 Baseline: full site build (brand, pages, DB, agent-editable)
Establishes the deploy baseline on main so the admin agent's publish/rollback
has a clean starting point. Everything built to date: sumi-e brand system,
homepage, projects (DB-driven case studies), resume, about, services +
website-design detail, contact form + DB, changelog, favicon + share card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DoFYZY9gkGPNDqZ7NuEa9a
2026-07-23 20:21:28 +00:00

2226 lines
56 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@import "tailwindcss" source(none);
@source "../src";
@import "tw-animate-css";
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400 700;
font-display: optional;
src: url("/fonts/inter-latin.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Fraunces";
font-style: normal;
font-weight: 400 700;
font-display: optional;
src: url("/fonts/fraunces-latin.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "JetBrains Mono";
font-style: normal;
font-weight: 400 500;
font-display: optional;
src: url("/fonts/jetbrains-mono-latin.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-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-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--font-serif: "Fraunces", ui-serif, Georgia, serif;
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
}
/* ============================================================
Carlos Arias — sumi-e brand palette
Ink on paper, vermillion as seal.
Written as hex so these values are eyeball-verifiable against
brand/BRAND.md, which is the source of truth. Do not "improve"
a colour here — change it in the brand guide first.
Two choices worth preserving:
· The ground is never pure white. Washi-toned.
· The near-black is blue-black, not neutral grey — real sumi
ink has a cool cast, and the whole ramp inherits it.
============================================================ */
:root {
--radius: 2px; /* near-square, not rounded */
--background: #f6f5f2; /* washi paper */
--foreground: #14151a; /* sumi ink */
--card: #f6f5f2;
--card-foreground: #14151a;
--popover: #efeeea;
--popover-foreground: #14151a;
/* Primary is INK, not red. The seal is never a button ground. */
--primary: #14151a;
--primary-foreground: #f6f5f2;
--secondary: #efeeea;
--secondary-foreground: #3d3f47;
--muted: #efeeea;
--muted-foreground: #6e7078;
/* Shuniku 朱肉 — seal ink. Target 12% coverage per screen. */
--accent: #d0342c;
--accent-foreground: #ffffff;
--destructive: #9b1c17; /* deeper than the seal, so
errors never read as brand */
--destructive-foreground: #ffffff;
--border: #dfddd7;
--input: #dfddd7;
--ring: #d0342c;
/* ---- brand extras the base theme has no slot for ---- */
--ca-ink-2: #3d3f47;
--ca-ink-3: #6e7078;
--ca-ink-4: #9a9ca3;
--ca-seal: #d0342c;
/* Motion. Wabi-sabi motion is slow and never demands attention.
Nothing bounces, nothing pulses, nothing loops in the viewport.
If an effect asks to be watched, it belongs to another brand. */
--ca-dur-quick: 180ms;
--ca-dur-base: 280ms;
--ca-dur-slow: 620ms;
--ca-dur-ink: 1500ms;
--ca-ease: cubic-bezier(0.33, 0.1, 0.25, 1);
--ca-ease-press: cubic-bezier(0.2, 0.9, 0.3, 1);
--ca-reveal-y: 12px;
--ca-stagger: 70ms;
}
.dark {
--background: #121317;
--foreground: #edece8;
--card: #121317;
--card-foreground: #edece8;
--popover: #191a1f;
--popover-foreground: #edece8;
--primary: #edece8;
--primary-foreground: #121317;
--secondary: #191a1f;
--secondary-foreground: #c2c1bd;
--muted: #191a1f;
--muted-foreground: #8b8d95;
/* Lifted on dark — #d0342c muddies against near-black.
Not a naive inversion. */
--accent: #e8564a;
--accent-foreground: #121317;
--destructive: #e8564a;
--destructive-foreground: #121317;
--border: #2a2c33;
--input: #2a2c33;
--ring: #e8564a;
--ca-ink-2: #c2c1bd;
--ca-ink-3: #8b8d95;
--ca-ink-4: #5e606a;
--ca-seal: #e8564a;
}
/* Ink-surface values exposed globally, so anything that has to sit on a dark
ground (the header while the mobile menu is open, an overlay, a badge) can
reach them without hardcoding a hex or relying on being nested inside
.ca-ink-surface. Mirrors brand/tokens.css — keep the two in step.
Note: an undefined var() invalidates the whole declaration silently, so a
missing name here shows up as "the colour just didn't apply", not an error. */
:root {
--ca-ink-surface-bg: #121317;
--ca-ink-surface-bg-2: #191a1f;
--ca-ink-surface-rule: #2a2c33;
--ca-ink-surface-fg: #edece8;
--ca-ink-surface-fg-2: #c2c1bd;
--ca-ink-surface-fg-3: #8b8d95;
--ca-ink-surface-fg-4: #5e606a;
--ca-ink-surface-seal: #e8564a;
}
/* ---- Ink surface ----
A section that stays ink-ground in BOTH themes, for committed dark
moments where the content itself demands darkness. Remaps the
semantic tokens locally, so anything nested inside needs no changes.
Use sparingly — one per page at most. */
/* IMPORTANT: this overrides the --color-* layer, NOT --background/--foreground.
The theme declares `--color-background: var(--background)` on :root, and a
custom property is substituted where it is DECLARED — so --color-background
resolved to paper at :root and only inherits from there. Redefining
--background on a descendant cannot retroactively change it, and the section
silently renders paper-on-paper.
(.dark works only because it sits on <html>, the same element as :root, so
the declaration re-resolves there. A nested scope has no such luck.)
Every utility — bg-background, text-muted-foreground, border-border — reads
the --color-* names, so overriding those is both correct and sufficient. */
.ca-ink-surface {
/* BOTH names are required, and for different consumers:
--foreground ← what Tailwind utilities compile to.
`@theme inline` emits `.text-foreground {
color: var(--foreground) }`, referencing the
underlying variable, not the alias.
--color-foreground ← what hand-written CSS in this file uses, and
what cannot be reached by overriding the raw
name alone (it was already substituted at :root).
Set one and half the section is wrong in a way that only shows up as
dark-on-dark text. Keep them in lockstep. */
--background: #121317;
--color-background: #121317;
--foreground: #edece8;
--color-foreground: #edece8;
--card: #121317;
--color-card: #121317;
--card-foreground: #edece8;
--color-card-foreground: #edece8;
--popover: #191a1f;
--color-popover: #191a1f;
--popover-foreground: #edece8;
--color-popover-foreground: #edece8;
--primary: #edece8;
--color-primary: #edece8;
--primary-foreground: #121317;
--color-primary-foreground: #121317;
--secondary: #191a1f;
--color-secondary: #191a1f;
--secondary-foreground: #c2c1bd;
--color-secondary-foreground: #c2c1bd;
--muted: #191a1f;
--color-muted: #191a1f;
--muted-foreground: #8b8d95;
--color-muted-foreground: #8b8d95;
--accent: #e8564a;
--color-accent: #e8564a;
--accent-foreground: #121317;
--color-accent-foreground: #121317;
--border: #2a2c33;
--color-border: #2a2c33;
--input: #2a2c33;
--color-input: #2a2c33;
--ring: #e8564a;
--color-ring: #e8564a;
--ca-ink-2: #c2c1bd;
--ca-ink-3: #8b8d95;
--ca-ink-4: #5e606a;
--ca-seal: #e8564a;
background-color: var(--color-background);
color: var(--color-foreground);
}
@layer base {
* {
border-color: var(--color-border);
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-sans);
font-feature-settings: "ss01", "cv11";
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
::selection {
background-color: color-mix(in oklab, var(--color-primary) 25%, transparent);
}
:where(a, button, input, textarea, select, summary):focus-visible {
outline: 2px solid var(--color-ring) !important;
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}
}
@layer components {
/* ============================================================
Brand primitives — defined ONCE, used everywhere.
Anything reused across more than one page belongs here, not in a
page's <style> block. If you find yourself pasting a .ca-* rule
into a second page, it was always meant to live here.
============================================================ */
/* Section band. Divided by a hairline whose break is filled with
vermillion — kintsugi. Wabi-sabi's actual philosophy is that the
repair is the beautiful part, not that the crack is hidden.
Offsets vary so no two seams sit in the same place. */
.ca-band {
position: relative;
border-top: 1px solid var(--color-border);
padding-block: clamp(4rem, 8vw, 6.5rem);
}
.ca-band::before {
content: "";
position: absolute;
top: -1px;
left: 12%;
height: 1px;
width: 28px;
background: var(--ca-seal);
}
.ca-band:nth-of-type(even)::before {
left: 63%;
width: 18px;
}
.ca-band:nth-of-type(3n)::before {
left: 34%;
width: 40px;
}
/* Index row. Work and writing are typographic indexes, not cards.
The seal rule becomes an interaction: exactly one red element on
screen at any moment, and only while pointed at. */
.ca-entry {
display: grid;
grid-template-columns: 1.5rem minmax(0, 1fr) auto;
align-items: baseline;
gap: 0 1.5rem;
padding-block: 1.6rem;
border-bottom: 1px solid var(--color-border);
text-decoration: none;
transition: border-color var(--ca-dur-base) var(--ca-ease);
}
.ca-entry:hover,
.ca-entry:focus-visible {
border-bottom-color: var(--ca-seal);
}
.ca-tick {
color: var(--ca-seal);
font-family: var(--font-mono);
opacity: 0;
transform: translateX(-0.4rem);
transition:
opacity var(--ca-dur-base) var(--ca-ease),
transform var(--ca-dur-base) var(--ca-ease);
}
.ca-entry:hover .ca-tick,
.ca-entry:focus-visible .ca-tick {
opacity: 1;
transform: translateX(0);
}
/* Theme-transition CSS is injected at runtime by the ThemeToggle React
component (src/components/ui/be-ui-theme-toggle.tsx), scoped to
html[data-beui-vt]. Do not add unconditional ::view-transition rules
here — they apply to every transition and fight the component's. */
/* ---- full-page mobile menu ----
The menu deliberately does NOT reach into the header. It used to lift the
header above the portal and strip its ground, which meant its correctness
depended on which header variant the page rendered — and the homepage has
its own [data-home-header] rules carrying !important that outrank anything
written here. Same component, two behaviours, every fix needed twice.
Instead the panel carries its own close control inside the portal, so it
behaves identically on every page by construction. */
.ca-menu-seal {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: var(--radius);
background: var(--ca-seal);
color: #fff;
font-family: var(--font-serif);
font-size: 1rem;
line-height: 1;
}
.ca-menu-item {
display: flex;
align-items: baseline;
gap: 1rem;
padding-block: 0.7rem;
font-family: var(--font-serif);
font-size: clamp(2.1rem, 11vw, 3.2rem);
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--color-foreground);
text-decoration: none;
}
.ca-menu-index {
margin-left: auto;
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.12em;
color: var(--ca-ink-4);
font-variant-numeric: tabular-nums;
}
.ca-menu-mail {
font-size: 1.05rem;
color: var(--color-foreground);
text-decoration: none;
border-bottom: 1px solid var(--ca-ink-4);
padding-bottom: 3px;
align-self: flex-start;
transition: border-color var(--ca-dur-base) var(--ca-ease);
}
.ca-menu-mail:hover {
border-bottom-color: var(--color-foreground);
}
/* ---- hero portrait ----
Masked with a radial gradient so the frame dissolves into the surrounding
ink rather than ending at a hard edge. That is the whole reason the hero
is a dark surface and not a dark rectangle on paper — there is no seam to
hide if the photograph never visibly stops. */
.ca-hero-portrait {
position: relative;
margin: 0;
max-width: 22rem;
justify-self: end;
aspect-ratio: 4 / 5;
overflow: hidden;
border-radius: var(--radius);
-webkit-mask: radial-gradient(125% 100% at 50% 42%, #000 48%, transparent 94%);
mask: radial-gradient(125% 100% at 50% 42%, #000 48%, transparent 94%);
}
.ca-hero-portrait img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: 50% 22%;
display: block;
filter: contrast(1.04) saturate(0.92);
}
/* Film grain, so the photograph shares the paper stock used everywhere else. */
.ca-hero-portrait::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
opacity: 0.1;
mix-blend-mode: overlay;
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'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}
/* The artist's seal, pressed on the work. */
.ca-hero-seal {
position: absolute;
z-index: 2;
right: 1rem;
bottom: 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.7rem;
height: 1.7rem;
border-radius: var(--radius);
background: var(--ca-seal);
color: #fff;
font-family: var(--font-serif);
font-size: 0.9rem;
line-height: 1;
}
@media (max-width: 63.99rem) {
.ca-hero-portrait {
justify-self: start;
max-width: 15rem;
}
}
/* ---- portrait ----
The frame reserves its space whether or not a photo exists, so dropping
the real image in later cannot reflow the page. 4:5 because a portrait
crop wants more height than a square and less than a full body shot. */
.ca-portrait {
display: block;
width: 100%;
aspect-ratio: 4 / 5;
object-fit: cover;
object-position: 50% 20%;
border-radius: var(--radius);
/* Left in colour deliberately. Earlier drafts graded portraits to
greyscale so a snapshot would sit inside the palette — but these were
shot for the page, and the warm practical lights behind the subject are
most of what makes the frame work. Greyscale would throw that away.
A light contrast lift is all it needs. */
filter: contrast(1.04) saturate(0.94);
}
/* Two portraits ship and CSS picks one. The theme is class-based, so a
<picture media="(prefers-color-scheme: dark)"> would follow the OS and
ignore the site's own toggle. */
.ca-portrait-dark {
display: none;
}
.dark .ca-portrait-light {
display: none;
}
.dark .ca-portrait-dark {
display: block;
}
.ca-portrait-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.6rem;
background: var(--color-secondary);
border: 1px dashed var(--color-border);
color: var(--ca-ink-4);
filter: none;
}
.ca-portrait-empty span {
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.18em;
text-transform: uppercase;
}
/* ---- about: Instagram reel rail ----
A 9:16 frame that sticks while the essay scrolls past it, so the media
stays in view alongside the reading. Ink-surface treatment because a Reel
is video and reads better on a dark ground than washi. */
.ca-reel-col {
align-self: start;
}
@media (min-width: 64rem) {
.ca-reel-col {
position: sticky;
top: 5.5rem;
}
}
.ca-reel {
position: relative;
margin: 0;
width: 100%;
max-width: 20rem;
aspect-ratio: 9 / 16;
overflow: hidden;
border-radius: var(--radius);
background: var(--ca-ink-surface-bg);
border: 1px solid var(--ca-ink-surface-rule);
}
.ca-reel-inner {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
color: var(--ca-ink-surface-fg-3);
}
.ca-reel-glyph {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
border: 1px solid var(--ca-ink-surface-fg-4);
border-radius: 50%;
font-size: 0.9rem;
padding-left: 0.2rem; /* optically centre the play triangle */
}
.ca-reel-label {
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ca-ink-surface-fg-3);
}
.ca-reel-handle {
font-size: 0.8rem;
color: var(--ca-ink-surface-fg-4);
}
/* wider measure for the about essay than default prose */
.ca-about-prose {
max-width: 62ch;
}
/* ---- about: timeline ---- */
.ca-timeline {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
}
.ca-timeline li {
display: grid;
grid-template-columns: 5.5rem minmax(0, 1fr);
gap: 1.5rem;
align-items: baseline;
padding: 1rem 0;
border-top: 1px solid var(--color-border);
}
.ca-timeline li:last-child {
border-bottom: 1px solid var(--color-border);
}
.ca-timeline-year {
font-family: var(--font-mono);
font-size: 0.8rem;
letter-spacing: 0.06em;
color: var(--ca-seal);
font-variant-numeric: tabular-nums;
}
.ca-timeline-note {
font-size: 0.95rem;
color: var(--ca-ink-2);
}
/* ---- portfolio (browser-frame mockups) ---- */
.ca-folio {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 2.5rem;
}
@media (min-width: 52rem) {
.ca-folio {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 2rem;
}
}
.ca-folio-item {
display: flex;
flex-direction: column;
text-decoration: none;
color: inherit;
}
.ca-folio-frame {
position: relative;
border: 1px solid var(--color-border);
border-radius: var(--radius);
overflow: hidden;
background: var(--color-secondary);
transition:
border-color var(--ca-dur-base) var(--ca-ease),
transform var(--ca-dur-base) var(--ca-ease);
}
.ca-folio-item:hover .ca-folio-frame,
.ca-folio-item:focus-visible .ca-folio-frame {
border-color: var(--ca-ink-3);
transform: translateY(-3px);
}
/* a small browser chrome bar above the screenshot */
.ca-folio-chrome {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.55rem 0.7rem;
border-bottom: 1px solid var(--color-border);
background: var(--color-background);
}
.ca-folio-chrome span {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: var(--ca-ink-4);
opacity: 0.5;
}
.ca-folio-frame img {
display: block;
width: 100%;
aspect-ratio: 1440 / 1024;
object-fit: cover;
object-position: top center;
}
.ca-folio-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
aspect-ratio: 1440 / 1024;
border: 1px dashed var(--color-border);
margin: 0.75rem;
border-radius: var(--radius);
color: var(--ca-ink-4);
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.ca-folio-glyph {
font-family: var(--font-serif);
font-size: 1.5rem;
line-height: 1;
}
.ca-folio-meta {
padding-top: 1rem;
}
.ca-folio-meta h3 {
font-family: var(--font-serif);
font-size: 1.25rem;
line-height: 1.2;
letter-spacing: -0.01em;
margin: 0;
}
.ca-folio-practice {
margin: 0.3rem 0 0;
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.06em;
color: var(--ca-ink-4);
text-transform: uppercase;
}
.ca-folio-note {
margin: 0.7rem 0 0;
font-size: 0.85rem;
line-height: 1.5;
color: var(--ca-ink-3);
}
.ca-folio-link {
display: inline-block;
margin-top: 0.7rem;
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.06em;
color: var(--ca-seal);
}
.ca-folio-item.is-empty {
opacity: 0.7;
}
/* ---- feature grid (what's included) ---- */
.ca-feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 0 3.5rem;
}
.ca-feature {
padding: 1.5rem 0;
border-top: 1px solid var(--color-border);
}
.ca-feature h3 {
font-family: var(--font-serif);
font-size: 1.35rem;
line-height: 1.2;
letter-spacing: -0.01em;
margin: 0 0 0.5rem;
}
.ca-feature p {
margin: 0;
font-size: 0.9rem;
line-height: 1.55;
color: var(--ca-ink-3);
max-width: 42ch;
}
/* ---- numbered process ---- */
.ca-process {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
}
.ca-process li {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1fr);
gap: 1.5rem;
padding: 1.5rem 0;
border-top: 1px solid var(--color-border);
}
.ca-process li:last-child {
border-bottom: 1px solid var(--color-border);
}
.ca-process-step {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--ca-seal);
font-variant-numeric: tabular-nums;
padding-top: 0.3rem;
}
.ca-process h3 {
font-family: var(--font-serif);
font-size: 1.35rem;
line-height: 1.2;
margin: 0 0 0.35rem;
}
.ca-process p {
margin: 0;
font-size: 0.9rem;
line-height: 1.55;
color: var(--ca-ink-3);
max-width: 54ch;
}
/* ---- services: verticals ---- */
.ca-verticals {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
gap: 0;
border-top: 1px solid var(--color-border);
}
.ca-vertical {
padding: 1.5rem 1.75rem 1.5rem 0;
border-bottom: 1px solid var(--color-border);
}
.ca-vertical h3 {
display: flex;
align-items: center;
gap: 0.6rem;
font-family: var(--font-serif);
font-size: var(--ca-s5);
line-height: 1.2;
letter-spacing: -0.01em;
margin: 0 0 0.5rem;
}
/* The primary vertical gets the seal — the one place the eye should land. */
.ca-vertical.is-primary h3 {
color: var(--ca-seal);
}
.ca-vertical-tag {
font-family: var(--font-mono);
font-size: 0.55rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #fff;
background: var(--ca-seal);
border-radius: var(--radius);
padding: 0.15rem 0.4rem;
}
.ca-vertical p {
margin: 0;
font-size: 0.9rem;
line-height: 1.55;
color: var(--ca-ink-3);
max-width: 40ch;
}
/* ---- changelog timeline ----
A single vermillion hairline runs down the left; each entry hangs a small
node off it. The node is the only red on the page per entry, which keeps
the timeline reading as a seam of marks rather than a rainbow of statuses.
Type is carried by a mono label, not by colour — except "bug found", which
takes the seal, because a bug is the one thing that should catch the eye. */
.ca-log {
position: relative;
}
.ca-log-group {
position: relative;
}
.ca-log-date {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin: 2.5rem 0 1.25rem;
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ca-ink-3);
}
.ca-log-group:first-child .ca-log-date {
margin-top: 0;
}
.ca-log-count {
font-size: 0.62rem;
color: var(--ca-ink-4);
font-variant-numeric: tabular-nums;
}
.ca-log-list {
margin: 0;
padding: 0;
list-style: none;
}
.ca-log-entry {
position: relative;
display: block;
padding: 0 0 1.6rem 1.75rem;
/* the vertical line, drawn per-entry so it stops cleanly at the last one */
border-left: 1px solid var(--color-border);
margin-left: 0.3rem;
}
.ca-log-entry:last-child {
border-left-color: transparent;
padding-bottom: 0;
}
.ca-log-node {
position: absolute;
left: -0.3rem;
top: 0.3rem;
width: 0.55rem;
height: 0.55rem;
border-radius: 50%;
background: var(--color-background);
border: 1px solid var(--ca-ink-4);
}
/* a bug is the one status that earns the seal */
.ca-log-node[data-type="bug"] {
background: var(--ca-seal);
border-color: var(--ca-seal);
}
.ca-log-node[data-type="fixed"],
.ca-log-node[data-type="added"] {
background: var(--ca-ink-3);
border-color: var(--ca-ink-3);
}
.ca-log-head {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin-bottom: 0.35rem;
}
.ca-log-type {
font-family: var(--font-mono);
font-size: 0.62rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ca-ink-4);
}
.ca-log-type[data-type="bug"] {
color: var(--ca-seal);
}
.ca-log-time {
font-family: var(--font-mono);
font-size: 0.62rem;
color: var(--ca-ink-4);
font-variant-numeric: tabular-nums;
}
.ca-log-summary {
margin: 0;
font-size: 0.95rem;
line-height: 1.5;
color: var(--color-foreground);
}
.ca-log-detail {
margin: 0.3rem 0 0;
font-size: 0.85rem;
line-height: 1.55;
color: var(--ca-ink-3);
max-width: 60ch;
}
/* ---- contact form ----
Underline fields, not boxes — a hairline that takes the seal on focus,
consistent with the rest of the site's ink-on-paper restraint. */
.ca-form {
display: flex;
flex-direction: column;
gap: 1.75rem;
max-width: 40rem;
}
.ca-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.ca-field label {
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ca-ink-4);
}
.ca-field input,
.ca-field textarea,
.ca-select select {
width: 100%;
border: 0;
border-bottom: 1px solid var(--color-border);
background: transparent;
padding: 0.6rem 0;
font-family: var(--font-sans);
font-size: 1rem;
color: var(--color-foreground);
border-radius: 0;
transition: border-color var(--ca-dur-base) var(--ca-ease);
}
.ca-field textarea {
resize: vertical;
min-height: 8rem;
line-height: 1.6;
}
.ca-field input:focus,
.ca-field textarea:focus,
.ca-select select:focus {
outline: none;
border-bottom-color: var(--ca-seal);
}
.ca-field input::placeholder,
.ca-field textarea::placeholder {
color: var(--ca-ink-4);
}
/* Native select restyled to match — caret is the ink arrow used elsewhere. */
.ca-select {
position: relative;
}
.ca-select select {
appearance: none;
-webkit-appearance: none;
cursor: pointer;
padding-right: 2rem;
}
.ca-select-caret {
position: absolute;
right: 0.25rem;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
color: var(--ca-ink-4);
font-size: 0.8rem;
}
.ca-select select option {
color: #14151a; /* option lists inherit the OS control colour, not ours */
}
.ca-field-error {
font-size: 0.8rem;
color: var(--ca-seal);
}
.ca-honeypot {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.ca-form-foot {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1.5rem;
margin-top: 0.5rem;
}
.ca-submit {
border: 1px solid var(--color-primary);
background: var(--color-primary);
color: var(--color-primary-foreground);
border-radius: var(--radius);
padding: 0.8rem 1.6rem;
font-family: var(--font-sans);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition:
background-color var(--ca-dur-base) var(--ca-ease),
color var(--ca-dur-base) var(--ca-ease),
opacity var(--ca-dur-base) var(--ca-ease);
}
.ca-submit:hover:not(:disabled) {
background: transparent;
color: var(--color-primary);
}
.ca-submit:disabled {
opacity: 0.55;
cursor: default;
}
.ca-form-status {
font-size: 0.875rem;
color: var(--ca-ink-3);
}
.ca-form-status[data-state="ok"] {
color: var(--ca-seal);
}
.ca-form-status[data-state="error"] {
color: var(--color-destructive);
}
/* ---- project detail ---- */
.ca-back {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ca-ink-4);
text-decoration: none;
transition: color var(--ca-dur-base) var(--ca-ease);
}
.ca-back:hover {
color: var(--color-foreground);
}
.ca-status {
display: inline-flex;
align-items: center;
gap: 0.4rem;
border-radius: var(--radius);
padding: 0.25rem 0.6rem;
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.12em;
text-transform: uppercase;
border: 1px solid var(--color-border);
color: var(--ca-ink-3);
}
/* Only a shipped thing gets the seal. "In development" is honest, not a badge. */
.ca-status.is-live {
border-color: var(--ca-seal);
color: var(--ca-seal);
}
.ca-chip {
border-radius: var(--radius);
border: 1px solid var(--color-border);
padding: 0.25rem 0.6rem;
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ca-ink-4);
text-decoration: none;
transition:
color var(--ca-dur-base) var(--ca-ease),
border-color var(--ca-dur-base) var(--ca-ease);
}
.ca-chip:hover {
color: var(--color-foreground);
border-color: var(--ca-ink-3);
}
.ca-facts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
gap: 0;
margin: 0;
border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
.ca-facts > div {
padding: 1.1rem 1.5rem 1.1rem 0;
}
.ca-facts dt {
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ca-ink-4);
}
.ca-facts dd {
margin: 0.4rem 0 0;
font-size: 0.9rem;
color: var(--color-foreground);
}
/* Numbers get the serif at display size — on a case study the result is the
headline, so it should be set like one. */
.ca-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
gap: 2.5rem;
}
.ca-metric-value {
font-family: var(--font-serif);
font-size: clamp(2.2rem, 5vw, 3.4rem);
line-height: 1;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.ca-metric-label {
margin-top: 0.6rem;
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ca-ink-3);
}
.ca-metric-note {
margin-top: 0.3rem;
font-size: 0.8rem;
color: var(--ca-ink-4);
max-width: 26ch;
}
/* ---- gallery ---- */
.ca-gallery {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
@media (min-width: 48rem) {
.ca-gallery {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.ca-gallery-item {
position: relative;
display: block;
width: 100%;
aspect-ratio: 4 / 3;
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-secondary);
padding: 0;
cursor: pointer;
}
.ca-gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform var(--ca-dur-slow) var(--ca-ease);
}
.ca-gallery-item:hover img,
.ca-gallery-item:focus-visible img {
transform: scale(1.03);
}
/* Empty placeholder tile — a dashed frame with its caption, so the gallery
reads as intentional structure rather than a broken image. */
.ca-gallery-item.is-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-style: dashed;
color: var(--ca-ink-4);
cursor: default;
}
.ca-gallery-glyph {
font-family: var(--font-serif);
font-size: 1.5rem;
line-height: 1;
}
.ca-gallery-cap {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 0.5rem 0.7rem;
background: linear-gradient(transparent, rgba(18, 19, 23, 0.72));
color: #fff;
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.08em;
text-align: left;
}
.ca-gallery-item.is-empty .ca-gallery-cap {
position: static;
background: none;
color: var(--ca-ink-4);
text-transform: uppercase;
}
/* ---- lightbox ---- */
.ca-lightbox {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
background: rgba(18, 19, 23, 0.92);
backdrop-filter: blur(4px);
}
.ca-lightbox-fig {
margin: 0;
max-width: 90vw;
max-height: 88vh;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.ca-lightbox-fig img {
max-width: 100%;
max-height: 82vh;
object-fit: contain;
border-radius: var(--radius);
}
.ca-lightbox-fig figcaption {
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.08em;
color: #c2c1bd;
text-align: center;
}
.ca-lightbox-close {
position: absolute;
top: 1rem;
right: 1.25rem;
width: 2.75rem;
height: 2.75rem;
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: 0;
color: #edece8;
font-size: 2rem;
line-height: 1;
cursor: pointer;
}
/* ---- skills applied (grouped disciplines) ---- */
.ca-skills {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
gap: 2.5rem 3.5rem;
}
.ca-skill-group h3 {
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ca-seal);
margin: 0 0 1rem;
}
.ca-skill-group ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
}
.ca-skill-group li {
padding: 0.55rem 0;
border-top: 1px solid var(--color-border);
font-size: 0.9rem;
color: var(--ca-ink-2);
}
.ca-skill-group li:last-child {
border-bottom: 1px solid var(--color-border);
}
/* ---- case-study body + social rail ---- */
.ca-body-grid {
display: grid;
gap: 2.5rem;
}
@media (min-width: 64rem) {
.ca-body-grid {
grid-template-columns: minmax(0, 16rem) minmax(0, 1fr);
gap: 4rem;
}
}
.ca-body-rail {
align-self: start;
}
@media (min-width: 64rem) {
/* Sticks alongside the case study as it scrolls. */
.ca-body-rail {
position: sticky;
top: 5.5rem;
}
}
/* Mobile: the reel leads the case study (it sits below the gallery, above
the body text). DOM order is already rail-then-body, so no order swap is
needed — just give the reel breathing room and cap its height so a tall
9:16 frame does not dominate the screen. */
@media (max-width: 63.99rem) {
.ca-body-rail {
margin-bottom: 1.75rem;
}
/* Beat the base .ca-reel max-width (20rem) with the rail scope + !important
is unnecessary here since two class selectors already outrank one. */
.ca-body-rail .ca-reel {
max-width: 15rem;
}
}
/* The prose column now defines its own measure via the grid, so it should
fill the track rather than cap at 68ch and leave the rail stranded. */
.ca-body-main {
max-width: 68ch;
}
/* ---- long-form body ---- */
.ca-prose {
max-width: 68ch;
}
.ca-prose h2 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 3vw, 2rem);
line-height: 1.2;
letter-spacing: -0.01em;
margin: 3rem 0 1rem;
}
.ca-prose h2:first-child {
margin-top: 0;
}
.ca-prose h3 {
font-family: var(--font-serif);
font-size: 1.3rem;
margin: 2.25rem 0 0.75rem;
}
.ca-prose p {
margin: 0 0 1.2rem;
line-height: 1.7;
color: var(--ca-ink-2);
}
.ca-prose ul,
.ca-prose ol {
margin: 0 0 1.4rem;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.ca-prose li {
position: relative;
padding-left: 1.1rem;
line-height: 1.6;
color: var(--ca-ink-2);
}
/* Same vermillion rule used as a marker in the CV — one list idiom, not two. */
.ca-prose li::before {
content: "";
position: absolute;
left: 0;
top: 0.72em;
width: 0.5rem;
height: 1px;
background: var(--ca-seal);
opacity: 0.75;
}
.ca-prose a {
color: var(--color-foreground);
border-bottom: 1px solid var(--ca-ink-4);
text-decoration: none;
}
.ca-prose strong {
font-weight: 600;
color: var(--color-foreground);
}
.ca-prose blockquote {
margin: 2rem 0;
padding-left: 1.5rem;
border-left: 2px solid var(--ca-seal);
font-family: var(--font-serif);
font-size: 1.25rem;
line-height: 1.4;
}
.ca-prose code {
font-family: var(--font-mono);
font-size: 0.85em;
background: var(--color-secondary);
padding: 0.1em 0.35em;
border-radius: var(--radius);
}
/* ---- filters ----
Chips, not buttons — each is a real href so a filtered view can be shared.
The active one takes the seal, which keeps the 12% rule intact: only ever
one filter is active, so only ever one red element. */
.ca-filters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding-bottom: 1.25rem;
border-bottom: 1px solid var(--color-border);
}
.ca-filter {
display: inline-flex;
align-items: baseline;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ca-ink-3);
text-decoration: none;
transition:
border-color var(--ca-dur-base) var(--ca-ease),
color var(--ca-dur-base) var(--ca-ease),
background-color var(--ca-dur-base) var(--ca-ease);
}
.ca-filter:hover {
border-color: var(--ca-ink-3);
color: var(--color-foreground);
}
.ca-filter[aria-current="true"] {
border-color: var(--ca-seal);
color: var(--ca-seal);
}
.ca-filter-count {
font-size: 0.62rem;
color: var(--ca-ink-4);
font-variant-numeric: tabular-nums;
}
.ca-filter[aria-current="true"] .ca-filter-count {
color: var(--ca-seal);
opacity: 0.7;
}
.ca-filter-empty {
margin-top: 2.5rem;
font-size: 0.875rem;
color: var(--ca-ink-3);
}
/* ---- filter transition ----
Scoped to [data-ca-filtering] so it cannot collide with the theme toggle,
which drives its own ::view-transition rules under html[data-beui-vt].
Rows that survive a filter glide to their new position; rows leaving fade
and settle downward, rows arriving rise into place. Nothing scales or
bounces — this is the same restraint as the rest of the motion here, just
applied to a list. */
html[data-ca-filtering]::view-transition-group(*) {
animation-duration: 420ms;
animation-timing-function: cubic-bezier(0.33, 0.1, 0.25, 1);
}
html[data-ca-filtering]::view-transition-old(*) {
animation:
ca-filter-out 260ms cubic-bezier(0.33, 0.1, 0.25, 1) both;
}
html[data-ca-filtering]::view-transition-new(*) {
animation:
ca-filter-in 380ms cubic-bezier(0.33, 0.1, 0.25, 1) 80ms both;
}
@keyframes ca-filter-out {
to {
opacity: 0;
transform: translateY(6px);
}
}
@keyframes ca-filter-in {
from {
opacity: 0;
transform: translateY(10px);
}
}
/* The root snapshot would otherwise cross-fade the entire page underneath
the row animations, which reads as a flicker. Let the named groups do the
work and leave the rest alone. */
html[data-ca-filtering]::view-transition-old(root),
html[data-ca-filtering]::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
@media (prefers-reduced-motion: reduce) {
html[data-ca-filtering]::view-transition-group(*),
html[data-ca-filtering]::view-transition-old(*),
html[data-ca-filtering]::view-transition-new(*) {
animation: none;
}
}
/* ---- CV rows ----
Two columns on desktop: dates parked left, substance right. Stacks on
mobile so the date never squeezes the role into a two-word column. */
.ca-cv-row {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.75rem 2.5rem;
padding-block: 2.25rem;
border-bottom: 1px solid var(--color-border);
}
@media (min-width: 48rem) {
.ca-cv-row {
grid-template-columns: 11rem minmax(0, 1fr);
}
}
.ca-cv-when {
display: flex;
align-items: center;
gap: 0.6rem;
padding-top: 0.4rem;
}
.ca-cv-now {
display: inline-flex;
align-items: center;
border-radius: var(--radius);
background: var(--ca-seal);
color: #fff;
padding: 0.1rem 0.4rem;
font-family: var(--font-mono);
font-size: 0.62rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.ca-cv-points {
margin: 1.1rem 0 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.45rem;
max-width: 64ch;
}
.ca-cv-points li {
position: relative;
padding-left: 1.1rem;
font-size: 0.875rem;
line-height: 1.55;
color: var(--ca-ink-3);
}
/* A short vermillion rule as the marker — the same kintsugi seam idea at
list scale, rather than a bullet character. */
.ca-cv-points li::before {
content: "";
position: absolute;
left: 0;
top: 0.68em;
width: 0.5rem;
height: 1px;
background: var(--ca-seal);
opacity: 0.75;
}
/* ---- navigation ----
The same sumi stroke used under headings, painted in left-to-right
on hover — the direction of writing, and the same gesture as the
signature. Reuses /brand/brush.svg rather than inventing a second
underline treatment.
Still obeys the seal rule: only one item can be hovered, so at most
one stroke plus the current-page stroke are ever on screen. */
.ca-nav-link {
position: relative;
padding-bottom: 4px;
}
.ca-nav-link::after {
content: "";
position: absolute;
left: -0.1em;
right: -0.14em;
bottom: 0;
height: 0.26em;
background-color: var(--ca-seal);
-webkit-mask: url("/brand/brush.svg") no-repeat left center / 100% 100%;
mask: url("/brand/brush.svg") no-repeat left center / 100% 100%;
clip-path: inset(0 100% 0 0);
transition: clip-path var(--ca-dur-slow) cubic-bezier(0.31, 0.06, 0.2, 1);
}
.ca-nav-link:hover::after,
.ca-nav-link:focus-visible::after,
.ca-nav-link[aria-current="page"]::after {
clip-path: inset(0 0 0 0);
}
/* Over the ink hero the seal needs the lifted red, same as the mark. */
[data-home-header]:not([data-scrolled="true"]):not([data-panel-open="true"])
.ca-nav-link::after {
background-color: var(--ca-ink-surface-seal, #e8564a);
}
@media (prefers-reduced-motion: reduce) {
.ca-nav-link::after {
transition: none;
}
}
/* Understated link — an ink rule that darkens rather than a colour change. */
.ca-quiet {
text-decoration: none;
border-bottom: 1px solid var(--ca-ink-4);
padding-bottom: 3px;
transition: border-color var(--ca-dur-base) var(--ca-ease);
}
.ca-quiet:hover {
border-bottom-color: var(--color-foreground);
}
/* A real sumi stroke: blunt where the brush lands, tapering as it
lifts, with kasure (dry-brush breaks) trailing the tail. Drawn as a
mask so the colour comes from the token and follows the theme.
Emphasis only — brushing every heading is the same as brushing none. */
.ca-brushed {
position: relative;
display: inline-block;
white-space: nowrap;
}
.ca-brushed::after {
content: "";
position: absolute;
left: -0.12em;
right: -0.18em;
bottom: -0.26em;
height: 0.3em;
background-color: var(--ca-seal);
-webkit-mask: url("/brand/brush.svg") no-repeat left center / 100% 100%;
mask: url("/brand/brush.svg") no-repeat left center / 100% 100%;
}
/* Delayed entrance, sequenced behind the signature writing itself. */
.ca-seq {
opacity: 0;
transform: translateY(var(--ca-reveal-y));
animation: ca-settle var(--ca-dur-slow) var(--ca-ease) var(--ca-seq-delay, 1.45s)
forwards;
}
@keyframes ca-settle {
to {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
.ca-seq {
opacity: 1;
transform: none;
animation: none;
}
.ca-tick {
opacity: 1;
transform: none;
}
}
.skip-link {
position: fixed;
left: 1rem;
top: 1rem;
z-index: 100;
border-radius: 6px;
background: var(--color-foreground);
color: var(--color-background);
padding: 0.65rem 0.85rem;
font-size: 0.875rem;
font-weight: 600;
transform: translateY(-160%);
transition: transform 150ms ease;
}
.skip-link:focus-visible {
transform: translateY(0);
}
/* ---- brand mark in the header ----
The signature is a single-colour black SVG, so it is inverted rather
than shipped twice. Three states to satisfy:
light theme, on paper → black (no filter)
dark theme, anywhere → white (invert)
home + not scrolled → white (invert) — it floats over the ink
hero, which is dark in BOTH themes
The last rule has to beat the first in light mode, so it is declared
after it and matched on the header's own data attributes. */
/* On the homepage the hero already carries the signature at full size,
so the header mark stays out of the way until you scroll past it.
Two signatures on one screen is redundancy, not branding. */
.ca-header-brand {
transition:
opacity var(--ca-dur-slow) var(--ca-ease),
transform var(--ca-dur-slow) var(--ca-ease);
}
[data-home-header]:not([data-scrolled="true"]):not([data-panel-open="true"])
.ca-header-brand {
opacity: 0;
transform: translateY(-6px);
pointer-events: none;
}
.ca-header-logo {
filter: none;
transition: filter var(--ca-dur-base) var(--ca-ease);
}
.dark .ca-header-logo {
filter: invert(1);
}
[data-home-header]:not([data-scrolled="true"]):not([data-panel-open="true"])
.ca-header-logo {
filter: invert(1);
}
.ca-header-seal {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.35rem;
height: 1.35rem;
flex: none;
border-radius: var(--radius);
background: var(--ca-seal);
color: #fff;
font-family: var(--font-serif);
font-size: 0.72rem;
line-height: 1;
transition: background-color var(--ca-dur-base) var(--ca-ease);
}
/* Over the ink hero the seal needs the lifted red — #d0342c muddies
against near-black. Same reason .dark uses a different value. */
[data-home-header]:not([data-scrolled="true"]):not([data-panel-open="true"])
.ca-header-seal {
background: var(--ca-ink-surface-seal, #e8564a);
}
[data-home-header] {
box-shadow: none;
backdrop-filter: none;
}
[data-home-header][data-scrolled="true"],
[data-home-header][data-panel-open="true"] {
background-color: var(--color-background) !important;
border-color: var(--color-border) !important;
color: var(--color-foreground);
}
[data-home-header] .home-header-link {
color: rgb(255 255 255 / 0.78);
transition: color 150ms ease;
}
[data-home-header] .home-header-brand {
color: rgb(255 255 255);
}
[data-home-header] .home-header-link:hover,
[data-home-header] .home-header-link.is-active {
color: rgb(255 255 255);
}
[data-home-header][data-scrolled="true"] .home-header-link,
[data-home-header][data-panel-open="true"] .home-header-link {
color: var(--color-muted-foreground);
}
[data-home-header][data-scrolled="true"] .home-header-brand,
[data-home-header][data-panel-open="true"] .home-header-brand {
color: var(--color-foreground);
}
[data-home-header][data-scrolled="true"] .home-header-link:hover,
[data-home-header][data-scrolled="true"] .home-header-link.is-active,
[data-home-header][data-panel-open="true"] .home-header-link:hover,
[data-home-header][data-panel-open="true"] .home-header-link.is-active {
color: var(--color-foreground);
}
[data-home-header] .home-header-action {
color: rgb(255 255 255 / 0.78);
}
[data-home-header] .home-header-action:hover,
[data-home-header] .home-header-action:focus-visible {
background-color: rgb(255 255 255 / 0.12);
color: rgb(255 255 255);
outline: none;
}
[data-home-header][data-scrolled="true"] .home-header-action,
[data-home-header][data-panel-open="true"] .home-header-action {
color: var(--color-muted-foreground);
}
[data-home-header][data-scrolled="true"] .home-header-action:hover,
[data-home-header][data-scrolled="true"] .home-header-action:focus-visible,
[data-home-header][data-panel-open="true"] .home-header-action:hover,
[data-home-header][data-panel-open="true"] .home-header-action:focus-visible {
background-color: var(--color-muted);
color: var(--color-foreground);
}
}
@utility font-serif {
font-family: var(--font-serif);
font-optical-sizing: auto;
}
@utility prose-article {
width: 100%;
max-width: 68ch;
min-width: 0;
font-size: 1.0625rem;
line-height: 1.75;
color: var(--color-foreground);
& h2 {
font-family: var(--font-serif);
font-size: 1.75rem;
font-weight: 600;
margin-top: 2.5rem;
margin-bottom: 1rem;
letter-spacing: -0.01em;
scroll-margin-top: 6rem;
}
& h3 {
font-family: var(--font-serif);
font-size: 1.35rem;
font-weight: 600;
margin-top: 2rem;
margin-bottom: 0.75rem;
scroll-margin-top: 6rem;
}
& p {
margin-bottom: 1.25rem;
}
& a {
color: var(--color-primary);
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-thickness: 1px;
}
& a:hover {
text-decoration-thickness: 2px;
}
& blockquote {
border-left: 2px solid var(--color-primary);
padding-left: 1.25rem;
margin: 1.5rem 0;
font-style: italic;
color: var(--color-muted-foreground);
font-family: var(--font-serif);
font-size: 1.15rem;
}
& ul,
& ol {
margin: 1.25rem 0;
padding-left: 1.5rem;
}
& ul {
list-style: disc;
}
& ol {
list-style: decimal;
}
& li {
margin-bottom: 0.5rem;
}
& code {
font-family: var(--font-mono);
font-size: 0.9em;
color: oklch(0.3 0.08 35);
background: oklch(0.95 0.012 85);
padding: 0.15rem 0.4rem;
border-radius: 4px;
}
.dark & code {
color: oklch(0.88 0.08 55);
background: oklch(0.23 0.006 80);
}
& .code-block {
position: relative;
width: 100%;
max-width: 100%;
min-width: 0;
margin: 1.5rem 0;
}
& pre {
box-sizing: border-box;
width: 100%;
max-width: 100%;
min-width: 0;
color: oklch(0.22 0.025 70) !important;
background: oklch(0.975 0.006 95) !important;
border: 1px solid oklch(0.88 0.008 80);
border-radius: 8px;
padding: 1rem 3.5rem 1rem 1.25rem;
overflow-x: auto !important;
margin: 0;
font-size: 0.9rem;
line-height: 1.6;
box-shadow: inset 0 1px 0 oklch(1 0 0 / 0.75);
}
.dark & pre {
color: oklch(0.91 0.012 85) !important;
background: oklch(0.18 0.006 80) !important;
border-color: oklch(0.33 0.006 80);
box-shadow: inset 0 1px 0 oklch(1 0 0 / 0.06);
}
& pre code {
display: block;
min-width: max-content;
color: inherit;
background: transparent;
padding: 0;
}
.dark & pre code {
color: inherit;
background: transparent;
}
& pre span {
color: oklch(0.28 0.035 70) !important;
}
.dark & pre span {
color: oklch(0.89 0.014 85) !important;
}
& .code-copy-button {
position: absolute;
top: 0.55rem;
right: 0.55rem;
z-index: 1;
display: inline-flex;
width: 2rem;
height: 2rem;
align-items: center;
justify-content: center;
border: 1px solid oklch(0.83 0.008 80);
border-radius: 6px;
color: oklch(0.46 0.015 70);
background: oklch(0.995 0.002 95 / 0.94);
transition:
color 150ms ease,
border-color 150ms ease,
background-color 150ms ease,
opacity 150ms ease;
}
& .code-copy-button:hover,
& .code-copy-button:focus-visible {
color: var(--color-foreground);
border-color: var(--color-primary);
outline: none;
}
& .code-copy-button::after {
position: absolute;
right: 0;
bottom: calc(100% + 0.4rem);
content: attr(data-tooltip);
pointer-events: none;
white-space: nowrap;
border-radius: 5px;
background: oklch(0.2 0.01 70);
color: oklch(0.98 0.002 90);
padding: 0.25rem 0.45rem;
font-family: var(--font-sans);
font-size: 0.72rem;
font-weight: 500;
line-height: 1.2;
opacity: 0;
transform: translateY(3px);
transition:
opacity 150ms ease,
transform 150ms ease;
}
& .code-copy-button:hover::after,
& .code-copy-button:focus-visible::after,
& .code-copy-button.is-copied::after {
opacity: 1;
transform: translateY(0);
}
.dark & .code-copy-button {
color: oklch(0.73 0.012 85);
background: oklch(0.24 0.006 80 / 0.94);
border-color: oklch(0.38 0.006 80);
}
.dark & .code-copy-button:hover,
.dark & .code-copy-button:focus-visible {
color: var(--color-foreground);
border-color: var(--color-primary);
}
& hr {
border: 0;
border-top: 1px solid var(--color-border);
margin: 2.5rem 0;
}
& img {
border-radius: 8px;
margin: 1.5rem 0;
}
}
@utility callout {
border-left: 3px solid var(--color-primary);
background: color-mix(in oklab, var(--color-primary) 6%, transparent);
padding: 1rem 1.25rem;
border-radius: 0 8px 8px 0;
margin: 1.5rem 0;
}