/* Shared visual language from the Living Signal sketch. */
:root {
  color-scheme: dark;
  /* Read by mini-sculpture.js (stylesReady): this sheet has applied. */
  --site-styled: 1;
  --paper: #101216;
  --ink: #f1f0e9;
  --muted: #a7adba;
  --rule: #2a303a;
  --accent: #9aacff;
  --surface: #1a202b;
  --site-font: Arial, Helvetica, sans-serif;
}
/* overflow-x: fanning page content briefly reaches past the right edge; never let it add a scrollbar. */
html { background: var(--paper); scrollbar-gutter: stable; overflow-x: hidden; }
/* Phones size the page to its widest content even past html's overflow-x: hidden, and a page that arrives
   wider than the one it left aborts the page transition ("Viewport size changed"). The Audio page's
   one-line card titles overflow on its first frame, before title-fit.js shrinks them. Clipping body keeps
   any passing overflow out of the page width; clip (not hidden) keeps body from becoming a scroller. */
body { overflow-x: clip; }
body { background: var(--paper); color: var(--ink); font-family: var(--site-font); font-weight: 400; }
button, input, select, textarea { font-family: var(--site-font); }
h1, h2, h3, h4 { font-family: var(--site-font); font-weight: 500; }
::selection { background: #9aacff45; color: var(--ink); }
a { transition: color 150ms ease; }
a.is-hover { color: var(--accent); }
/* Hover colour only where a pointer can hover: on a touch screen :hover sticks on after a tap. Every
   :hover rule in the site's stylesheets sits under this query (tests/css-guards.test.cjs). */
@media (hover: hover) { a:hover { color: var(--accent); } }
/* Touch feedback instead: no grey tap flash, and a press shows the hover colour (iOS applies :active on
   touch only while a touchstart listener exists on the page; mini-sculpture.js adds one everywhere). */
a, button { -webkit-tap-highlight-color: transparent; }
@media (hover: none) {
  .shell > header nav a:active, .audio-sections a:active, .site-footer a:active { color: var(--accent); transition: none; }
  .audio-sections a:active { border-color: var(--accent); }
  .audio-catalog a.media:active { box-shadow: 0 0 16px 2px #9aacff24, 0 0 34px #8976ef18; transition: none; }
  .back-to-top:active { color: #f3f6ff !important; border-color: #8092b7; }
}
a:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
/* The side gutters never fall under a notch in landscape (viewport-fit=cover on every page). */
.shell { max-width: 1720px; padding-inline: max(clamp(22px, 4.7vw, 80px), env(safe-area-inset-left)) max(clamp(22px, 4.7vw, 80px), env(safe-area-inset-right)); }
.shell > header { padding: 32px 0; align-items: center; border-bottom: 1px solid var(--rule); }
.shell > header .identity { font-size: 22px; font-weight: 500; letter-spacing: -.055em; }
.shell > header nav { padding: 0; gap: 32px; font-size: 16px; width: auto; }
/* The line height is the old 14px text's, so the larger links leave the header's height as it was (likewise below). */
.shell > header nav a { border-bottom: 1px solid transparent; line-height: 21px; }
.shell > header nav a[aria-current="page"] { color: var(--accent); border-color: var(--accent); }
/* Leaving for another page, the highlight moves to its link over the leave (mini-sculpture.js navTo),
   so the next page arrives with it already in place. */
.nav-leaving .shell > header nav a { transition: color var(--nav-leave, .4s) cubic-bezier(.45, 0, .55, 1), border-color var(--nav-leave, .4s) cubic-bezier(.45, 0, .55, 1); }
.nav-leaving .shell > header nav a[aria-current="page"]:not(.is-next) { color: var(--ink); border-color: transparent; }
.shell > header nav a.is-next { color: var(--accent); border-color: var(--accent); }
/* Arriving from a trip that runs entirely on this page (mini-sculpture.js navFrom): the previous page's
   link starts highlighted and the highlight moves over the trip. */
.nav-settling .shell > header nav a { transition: color var(--nav-leave, .4s) cubic-bezier(.45, 0, .55, 1), border-color var(--nav-leave, .4s) cubic-bezier(.45, 0, .55, 1); }
.nav-arriving .shell > header nav a { transition: none; }
/* Hover stays as quick during a trip as at rest; only the highlight takes the trip's slow fade.
   .is-hover and .is-unhover stand in for :hover while the transition holds the pointer (mini-sculpture.js). */
:is(.nav-leaving, .nav-settling) .shell > header nav a:is(:hover, .is-hover, .is-unhover) { transition: color 150ms ease, border-color var(--nav-leave, .4s) cubic-bezier(.45, 0, .55, 1); }
.nav-arriving .shell > header nav a[aria-current="page"]:not(.is-prev) { color: var(--ink); border-color: transparent; }
.shell > header nav a.is-prev { color: var(--accent); border-color: var(--accent); }
.site-header { view-transition-name: site-header; }
@view-transition { navigation: auto; }
/* Never opt the root out (html { view-transition-name: none }): on WebKit the title morph's screen blend
   (.title-morph-blend, its group) needs the root's new image as its backdrop inside the pseudo tree, and
   without it the melt's black ground showed as a box for the whole trip (2026-10-06). */
::view-transition-old(root), ::view-transition-new(root) { animation-duration: 240ms; }
/* Chromium sometimes captures the outgoing page as a blank white snapshot. The sculpture carries
   itself across and content fades, fans or folds on its own, so never show that snapshot, and let
   the incoming page appear at once rather than fading the sculpture along with it. */
::view-transition-old(root) { display: none; }
::view-transition-new(root) { animation: none; }
/* The title's trip between pages is drawn by mini-sculpture.js on the sculpture's clock. */
.title-morph { position: fixed; z-index: 6; pointer-events: none; overflow: visible; }
.title-morph-word { position: absolute; top: 0; left: 0; transform-origin: 0 0; white-space: nowrap; will-change: transform, filter, opacity; }
.title-morph-word span { display: block; }
/* WebKit: the melt's threshold runs on the GPU as brightness/contrast over a black layer that is screen-blended
   away, in the page and inside its view-transition group alike (mini-sculpture.js, WEBKIT). */
.title-morph-blend { mix-blend-mode: screen; } /* its black ground is inline, only while the threshold runs (mini-sculpture.js) */
html.is-webkit::view-transition-group(title-morph) { mix-blend-mode: screen; }
/* Layers during a page trip (mini-sculpture.js): the previous page's carried snapshots sit above the
   page, and cards fanning in as snapshots sit above those, so the homepage's controls tuck away
   behind the arriving cards (each card also carries a paper plate while it moves); the flying title
   and the header stay on top. */
::view-transition-group(*.carry) { z-index: 1; }
/* Where the arriving page can't animate the carried snapshots itself (no animate() on view-transition
   pseudo-elements), they fade on the trip's clock instead of the default quarter second. */
::view-transition-old(*.carry) { animation-duration: .7s; animation-timing-function: cubic-bezier(.37, 0, .63, 1); }
::view-transition-group(*.arrive) { z-index: 2; }
/* The flying title (mini-sculpture.js) stays above the cards and their paper plates; it moves itself. */
::view-transition-group(title-morph) { z-index: 3; }
::view-transition-group(title-morph), ::view-transition-image-pair(title-morph), ::view-transition-new(title-morph), ::view-transition-old(title-morph) { animation: none; }
::view-transition-group(site-header) { z-index: 4; }
::view-transition-group(site-header), ::view-transition-new(site-header) { animation: none; }
::view-transition-old(site-header) { animation: none; opacity: 0; }

.skip { z-index: 20; background: var(--surface); color: var(--ink); border-radius: 6px; }

.page-heading { padding: 64px 0 42px; border-top: 0; }
.page-heading h1 { font-size: clamp(2.75rem, 6.7vw, 6rem); font-weight: 500; letter-spacing: -.065em; line-height: 1.02; overflow-wrap: anywhere; }

.audio-page-heading { align-items: flex-end; flex-wrap: wrap; }
.audio-sections { gap: 26px; font-size: 16px; }
.audio-sections a { color: #bbc3d2; border-color: var(--rule); }
@media (hover: hover) { .audio-sections a:hover { color: var(--accent); border-color: var(--accent); } }
.audio-category > h2 { font-size: clamp(1.375rem, 2.5vw, 2rem); font-weight: 500; letter-spacing: -.04em; }
.project-title :is(h2,h3,h4) { font-weight: 500; letter-spacing: -.025em; color: var(--ink); }
.project .project-credit { color: var(--muted); }
.media, .media.alt, .app-media, .app-media.workflow { background: var(--surface); border-color: var(--rule); }
.audio-catalog .media { border-radius: 10px; }
/* Soft lavender glow leaves the artwork, crop, and card geometry untouched. */
.audio-catalog a.media { transition: box-shadow 260ms ease; }
.audio-catalog a.media:focus-visible { box-shadow: 0 0 16px 2px #9aacff24, 0 0 34px #8976ef18; }
@media (hover: hover) {
  .audio-catalog a.media:hover { box-shadow: 0 0 16px 2px #9aacff24, 0 0 34px #8976ef18; }
}

/* The original text stays readable underneath an inert colour layer: a static gradient clipped to a
   copy of the lettering (reduced motion, paused), or the live fluid canvas masked by the lettering
   (title-light.js), which the compositor shows directly. */
[data-title-glow]:not(.signal-name) { position: relative; }
.title-glow-layer, .title-glow-field {
  position: absolute; inset: 0; z-index: 1; display: block; margin: 0; padding: 0;
  pointer-events: none; user-select: none; opacity: 0;
  filter: drop-shadow(0 0 4px #a48aff44) drop-shadow(0 0 12px #9078ee28);
}
.title-glow-layer {
  font: inherit; letter-spacing: inherit; text-align: inherit; white-space: inherit; overflow-wrap: inherit;
  color: transparent; -webkit-text-fill-color: transparent;
  background-repeat: no-repeat; background-size: 100% 100%;
  background-clip: text; -webkit-background-clip: text;
}
.title-glow-line { display: block; width: max-content; margin-inline: auto; }
.title-glow-canvas {
  position: absolute; display: block;
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
/* Where WebGL masks the fluid itself (.title-glow-direct), the glow's two shadows are split between
   the canvas and its field: the same result, pixel for pixel, as the chained pair, for a fraction of
   the GPU time (the compositor re-blurs the live canvas every frame). A CSS mask would cut the
   canvas's own shadow away, so the CSS fallback keeps the chain. */
.title-glow-direct { filter: drop-shadow(0 0 12px #9078ee28); }
.title-glow-direct .title-glow-canvas { filter: drop-shadow(0 0 4px #a48aff44); }
/* Collection titles keep the fluid within the letters, without an outline glow. */
.page-heading :is(.title-glow-layer, .title-glow-field, .title-glow-canvas) { filter: none; }
/* Background fragment of the homepage sculpture (mini-sculpture.js). Page content paints above it. */
.page-sculpture { position: absolute; top: 0; left: 0; width: 100%; overflow: hidden; pointer-events: none; z-index: 0; }
.page-sculpture canvas { display: block; }
.page-sculpture ~ .shell { position: relative; z-index: 1; }
/* The sculptures turn by dragging (living-signal.js, mini-sculpture.js). A collection page's heading
   over its sculpture is never selected, the browser only pans vertically there so sideways drags reach
   the page, and the grab cursor shows wherever a drag can start. */
.page-heading[data-sculpture] { touch-action: pan-y; touch-action: pan-y pinch-zoom; } /* declared twice: a browser that rejects the pair keeps pan-y */
.page-heading[data-sculpture] h1 { -webkit-user-select: none; user-select: none; }
html.sculpture-grab, html.sculpture-grab * { cursor: grab; }
html.sculpture-dragging, html.sculpture-dragging * { cursor: grabbing !important; }
/* The sculpture morphs itself across pages. It starts exactly where the old one was, so the page crossfade between them is invisible, and it keeps its normal place behind the heading. */
.sculpture-morph { position: fixed; top: 0; left: 0; pointer-events: none; }
/* Arriving home, the overlay draws the lights until it hands over to the real ones. */
.sculpture-arriving .signal-light { opacity: 0 !important; transition: none !important; }
.sculpture-arrived .signal-light { transition: none !important; }

.app-copy h2, .app-copy h3 { font-size: clamp(1.5rem, 2.3vw, 2rem); font-weight: 500; letter-spacing: -.045em; line-height: 1.15; color: var(--ink); }
.app-copy p { color: var(--muted); line-height: 1.65; }
.app-copy .role { color: var(--accent); font-size: 14px; line-height: 1.5; }
.app-screenshot { background: transparent; border: 0; }
.app-screenshot img { border-radius: 10px; }
.app-project + .app-project { border-color: var(--rule); }
/* Divider lines drawn as their own element (mini-sculpture.js RULED), so they fan with the page's
   cards; the border keeps its space, and stays the line wherever the script doesn't run. */
:is(.app-project + .app-project, .experience, .audio-category + .audio-category):has(> .fan-rule) { position: relative; border-top-color: transparent; }
.fan-rule { position: absolute; left: 0; right: 0; top: -1px; height: 1px; background: var(--rule); pointer-events: none; }

.about-portrait img { border-radius: 12px; }
.about-body .bio-paragraph { color: var(--ink); font-weight: 400; letter-spacing: -.02em; }
.about-body .bio-paragraph strong { font-weight: 500; }
.experience h2 { color: var(--muted); font-weight: 500; letter-spacing: -.015em; }
.experience-details h3 { font-weight: 500; letter-spacing: -.025em; color: var(--ink); }
.experience-company { color: #c4c9d5; }
.experience-location, .experience-dates, .about-body .about-location { color: var(--muted); }
/* Keep supplied dark logos legible without changing the artwork. */
.experience-logo:not(.experience-logo--boombox) { background: #f1f0e9; border-radius: 6px; padding: 5px; }
.experience-logo--boombox { border-radius: 6px; }

.site-footer { margin: 0; padding: 22px 0; color: #959eaf; border-color: var(--rule); }
.site-footer a { color: #bbc3d2; }
@media (hover: hover) { .site-footer a:hover { color: var(--accent); } }
.profile-icon { height: 20px; }
.profile-icon-cutout { fill: var(--paper); }

@media (max-width: 700px) {
  .shell > header { padding: 12px 0 8px; gap: 6px; align-items: flex-start; }
  .shell > header .identity { font-size: 23px; }
  .shell > header nav { gap: 28px; font-size: 16px; flex-wrap: wrap; width: 100%; }
  .shell > header nav a { padding: 5px 0; line-height: 24px; }
  .page-heading { padding: 44px 0 34px; gap: 24px; }
  /* The six sections sit beside the heading's sculpture as a clean block: two columns of equal-width links
     (one underline length, just past the longest label), so the rows square up. The negative top
     margin centres the block on the title while the title keeps its place from the other pages, so it
     doesn't shift on trips between them; below, the block keeps the heading's usual gap. */
  .audio-page-heading { flex-direction: row; align-items: flex-start; justify-content: space-between; gap: 16px; }
  .audio-sections { display: grid; grid-template-columns: repeat(2, 80px); gap: 2px 30px; width: auto; flex: none; margin-top: -25px; font-size: 14px; }
  .audio-sections a { text-align: center; padding: 4px 0; }
  .site-footer { padding: 18px 0 calc(26px + env(safe-area-inset-bottom)); }
  .audio-category > h2 { font-size: 1.25rem; }
}
/* Tablets: the header and the section links at the phone sizes rather than the desktop ones. */
@media (min-width: 701px) and (max-width: 1100px) {
  .shell > header .identity { font-size: 26px; }
  .shell > header nav { gap: 36px; font-size: 16px; }
  .shell > header nav a { padding: 6px 0; line-height: 25.5px; }
  .audio-sections { gap: 30px; font-size: 16px; }
  .audio-page-heading { flex-wrap: wrap; row-gap: 24px; }
}
@media (max-width: 350px) {
  .shell > header nav { gap: 20px; font-size: 15px; }
  .shell > header nav a { line-height: 22.5px; }
}
/* Landscape phones: a one-line header (about 49px), so the short screen goes to the sculpture
   (living-signal.css budgets on it). After the phone block so it wins on phones under 700px wide too. */
@media (hover: none) and (max-height: 500px) {
  .shell > header { padding: 8px 0 6px; gap: 16px; flex-wrap: nowrap; align-items: center; }
  .shell > header .identity { font-size: 20px; }
  .shell > header nav { width: auto; gap: 22px; font-size: 15px; flex-wrap: nowrap; }
  .shell > header nav a { padding: 5px 0; line-height: 24px; }
}
@media (prefers-reduced-motion: reduce) {
  a { transition: none; }
  .audio-catalog a.media { transition: none; }
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none; }
}
