/* GlitchCraft Engine — shared design tokens.
   Spacing/radius/container scale is deliberately fixed here; each site's
   colors/fonts come from its own theme block in base.leaf's <style>. */

:root {
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 20px;

  --container: 1180px;
  --prose-width: 68ch;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body), -apple-system, BlinkMacSystemFont, sans-serif;
  line-height: 1.6;
}

/* Background depth for jford is now the terminal-window (see
   catalog.css / terminal-window.leaf) — the mesh-blob + grain-texture
   mechanism that used to live here (opt-in via theme.mesh-secondary) was
   removed along with it. Checked before deleting: no other site's
   site.json set mesh-secondary or bg-gradient, so nothing else depended
   on it. */

/* Opt-in colorful body background (theme.gradient-bg — currently just
   jford), same gating pattern as .has-terminal-companion. Three soft
   blobs, one per accent token, each mixed only 32-38% into --bg rather
   than used at full strength — enough to read as "colorful" without
   getting so saturated that --ink text loses contrast directly on top of
   it, or that the glass cards below (which lighten whatever's behind
   them toward white) end up sampling something too vivid. Falls back to
   --accent when a site only defines one accent color (accent-2/3 both
   alias to --accent via the fallback in var(), same pattern used by
   .terminal-glow), so this wouldn't break if some other single-accent
   site opted in later. */
body.has-gradient-bg {
  background:
    radial-gradient(circle at 15% 10%, color-mix(in srgb, var(--accent-3, var(--accent)) 36%, var(--bg)) 0%, transparent 45%),
    radial-gradient(circle at 85% 20%, color-mix(in srgb, var(--accent-2, var(--accent)) 32%, var(--bg)) 0%, transparent 50%),
    radial-gradient(circle at 50% 95%, color-mix(in srgb, var(--accent) 34%, var(--bg)) 0%, transparent 55%),
    var(--bg);
  background-attachment: fixed;
}

.container {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 var(--space-5);
}

/* Real page content needs its own stacking context above any full-bleed
   `position: fixed` ambient effect (grid-glow/star-field/noise-overlay, or
   a site's own bespoke background) — per CSS paint order, non-positioned
   block content (main/footer's default) paints BEHIND a positioned
   z-index:0 sibling regardless of DOM order, not above it just because it
   comes later in the document. Invisible for a faint effect like
   grid-glow's 3%-opacity lines, but a fully broken (content-hidden) page
   for an opaque one like a WebGL canvas background — caught while wiring
   glitchcraft.studio's background3d.js up to these effects. */
main, .site-footer {
  position: relative;
  z-index: 1;
}

/* ---- nav ----
   Sticky by default for every site — this is the general "always-visible
   header" every site should have, not just the video-hero one. Hyde's
   floating-nav (fixed, starts invisible over the full-bleed video, fades
   in on scroll — see catalog.css body.has-floating-nav + floating-nav.js)
   is a distinct enhancement layered on top via higher specificity; it
   overrides position/background/opacity but nothing here conflicts with
   it needing to. */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 40;
  background: color-mix(in srgb, var(--bg) 76%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 15%, transparent);
}
.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
}
.brand {
  font-family: var(--font-display), Georgia, serif;
  font-weight: 600;
  font-size: 1.1rem;
  color: var(--ink);
  text-decoration: none;
}
.nav-inner nav { display: flex; gap: var(--space-5); flex-wrap: wrap; }
.nav-inner nav a {
  color: var(--ink);
  opacity: 0.75;
  text-decoration: none;
  font-size: 0.92rem;
}
.nav-inner nav a[aria-current="page"] { opacity: 1; color: var(--accent); }

/* ---- hero ---- */
.hero {
  padding: var(--space-8) 0 var(--space-7);
  text-align: center;
  position: relative;
  overflow: hidden;
}
/* A quiet wash of the site's own accent behind the hero text — the one
   place every theme gets to show its color prominently, per feedback that
   a flat single-color hero reads as unfinished rather than restrained. */
.hero::before {
  content: "";
  position: absolute;
  inset: -20% -10% auto -10%;
  height: 140%;
  background: radial-gradient(ellipse at 50% 0%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%);
  pointer-events: none;
  z-index: 0;
}
.hero-video::before,
.hero-image::before { content: none; } /* the video/image already carries the visual weight */
.hero-inner { position: relative; z-index: 1; }
.hero .eyebrow {
  font-family: monospace;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.75rem;
  color: var(--accent);
  margin: 0 0 var(--space-3);
}
.hero h1 {
  font-family: var(--font-display), Georgia, serif;
  font-size: clamp(2rem, 5vw, 3.2rem);
  margin: 0;
}
.hero-tagline {
  font-size: 1.75rem;
  opacity: 0.7;
  font-family: var(--font-display), Georgia, serif;
  margin: 0;
}

/* ---- profile image (between the hero and the first section) ---- */
.profile-image-container { max-width: 240px; margin: 0 auto var(--space-7); margin-bottom: 0; text-align: center; }
.profile-image {
  width: 100%;
  display: block;
  border-radius: 100%;
  border: 4px solid var(--bg);
  box-shadow: 0 4px 20px color-mix(in srgb, var(--ink) 25%, transparent);
}

/* ---- sections ----
   Every other section gets a full-bleed tinted band (set server-side —
   SectionViewModel.alternate — not nth-child, since heroes are <section>
   too and would throw off a CSS-only odd/even count). Without this a page
   with several sections back to back (six on jford now) reads as one
   continuous wall with no sense of where one ends and the next begins. */
.content-section { padding: var(--space-7) 0; }
.content-section.section-alt {
  background: color-mix(in srgb, var(--ink) 3%, transparent);
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}
.section-title {
  font-family: var(--font-display), Georgia, serif;
  font-size: 1.6rem;
  margin: 0 0 var(--space-3);
}
.lede { color: color-mix(in srgb, var(--ink) 70%, transparent); max-width: var(--prose-width); }

.content-container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-5);
}
/* Both of these cap width narrower than their parent .container (1180px)
   — max-width alone just left-aligns a narrower block inside a wider
   one, leaving empty space on one side instead of centering it. Audited
   every other max-width added this session for the same missing
   `margin: 0 auto` after finding it here (see round-4 commit message for
   the full list: video-item's poster/player had it too; .lede and
   .timeline-photo were checked and left alone deliberately — they're
   flowing text/media matching a left-aligned heading beside them, not
   standalone blocks, so centering them would look wrong instead of
   fixing anything). */
.content-container.timeline { grid-template-columns: 1fr; max-width: var(--prose-width); margin: 0 auto; }
/* Wider than pure prose-width — flow sections hold side-by-side
   text+photo feature cards (see .feature-card below), which need more
   room than a single reading column. Solo/trailing text and panels still constrain
   themselves individually (panels via their own padding, plain text via
   the direct-child rule below) so nothing sprawls to the full width. */
.content-container.flow { grid-template-columns: 1fr; max-width: 900px; gap: var(--space-6); }
.content-container.flow > .text-item { max-width: var(--prose-width); margin: 0 auto; }

/* Plain text items read as captions/asides, not full-weight grid cells —
   they're the lightest-weight content an items section can hold. */
.text-item { margin: 0; font-size: 0.92rem; opacity: 0.75; align-self: center; }
/* In a flow section, text items are the real body copy (e.g. a project's
   paragraph beside its photo), not lightweight asides — full weight and
   real typographic care (size/line-height), not the caption treatment
   above, since this now sits next to an image as a design element. */
.content-container.flow .text-item {
  font-size: 1.05rem;
  line-height: 1.7;
  opacity: 1;
}

/* Each paragraph (plus every photo immediately following it — see
   SiteController.buildFeatureCards) is its own bounded card, stacked
   vertically: photo(s) full-width at the top, then the title, then the
   body text — one visual unit reading top-to-bottom, not a side-by-side
   split (photo column vs. text column) that read as two disconnected
   halves rather than one thing. Lighter than .panel below — a project's
   intro should still read as the bold lead-in, these as the supporting
   detail underneath it, in a grid (see .project-cards).

   Glass material (round 2): translucent white over blur instead of a
   flat ink tint — only reads as "glass" now that .has-gradient-bg gives
   it something varied to blur. White (not --ink or --bg) mixed to ~55%,
   matching the inspiration's own ~0.5-0.55 white-alpha cards — since
   glass always lightens whatever's behind it toward its own tint, using
   white here is what keeps --ink text legible regardless of which part
   of the gradient a given card happens to sit over. Neither .feature-card
   nor .project-group .panel is reachable outside jford's flow-layout
   content today, so this doesn't need its own body-class gate the way
   the gradient itself does. */
.feature-card {
  background: color-mix(in srgb, white 55%, transparent);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid color-mix(in srgb, white 60%, transparent);
  border-radius: var(--radius-md);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.feature-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.12);
}
.feature-card-body {
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
/* Was .feature-card-photos (photo-only) before explicit `group` items
   introduced mixed photo+video cards — renamed alongside
   ItemViewModel.photos -> media. Rendered last in the card now (after
   title/date/text/link — see feature-item.leaf). Row layout (wrap
   allowed) so multiple items (Atlasify's 2 photos, Vision Pro's
   photo+2videos) sit side by side sharing the row instead of stacking.

   width: 100% (not a small fixed max-width) — items are sized by their
   own real aspect ratio now (flex: 0 0 auto + a fixed height, see
   .photo-item/.video-item below), not by growing to fill an arbitrary
   row width, so a small cap just forced multi-item rows to wrap far
   before they needed to: checked the real math against .feature-card's
   actual available width (.content-container.flow's 900px, minus
   .feature-card's 2px border and .feature-card-body's 48px padding,
   border-box throughout, = ~850px) rather than assuming a fixed value
   would work — Atlasify's 2 real photos (2048x1426, 996x726) and Vision
   Pro's photo+2videos (1084x906, a square 1366x1366, 1280x720) both
   genuinely need several hundred px more than the old 360/480px cap ever
   gave them. flex-wrap stays on as a real fallback (a card with enough
   items, or a narrow/mobile viewport) rather than something that fired
   this early — no separate mobile override needed any more either, 100%
   already means 100% of whatever .feature-card-body's width is at any
   viewport. */
/* align-items: flex-start (not the flex row default of stretch) — stretch
   was forcing every item in the row to the row's tallest item's height
   regardless of its own aspect ratio, which is what the object-fit:cover
   below was then cropping to fill. Each item now gets a fixed height of
   its own (see .photo-item/.video-item below) instead, so cross-axis
   stretching has nothing useful left to do and would only fight that. */
.feature-card-media {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-3);
  width: 100%;
}
/* flex: 0 0 auto + a fixed height + width: auto — each item's width comes
   from its own real aspect ratio at that shared height (flex-basis: auto
   sizes a flex item to its content's intrinsic width when width itself
   is auto), not from splitting the row's width evenly. */
.feature-card-media .photo-item { flex: 0 0 auto; height: 220px; width: auto; }
/* The flex-row sizing (flex/width) now targets .video-item-container —
   that's the actual direct child of .feature-card-media's row since the
   caption fix above, not the figure any more (the figure is nested one
   level deeper inside it). align-items: flex-start overrides the column's
   own default stretch (see the base .video-item-container rule) so the
   figure inside sizes from its own aspect-ratio instead of being
   stretched to the container's row-cross-axis width — same
   fixed-height/auto-width mechanism as the photo case above, just via a
   nested flex column rather than a single flex item, since this one
   also needs to make room for an optional caption above the figure
   without the caption affecting the figure's own aspect-ratio box. */
.feature-card-media .video-item-container {
  max-width: none;
  flex: 0 0 auto;
  width: auto;
  align-items: flex-start;
}
.feature-card-media .video-item { height: 220px; }
/* 3+-item rows need more headroom than 220px leaves inside the card's
   real available width to stay on one line — checked the math, didn't
   assume: Vision Pro's photo+2videos (1084x906, a square 1366x1366,
   1280x720) needs ~898px at 220px height, genuinely over the ~850px
   available (.content-container.flow's 900px minus .feature-card's 2px
   border and .feature-card-body's 48px padding, border-box throughout)
   even with the row's own max-width cap removed entirely above. Scoped
   to 3+ items specifically (:has selects a 3rd direct child) rather than
   lowering the height everywhere: Atlasify's 2 photos and Live
   Activities' photo+video already fit fine at the full 220px, so the
   common 1-2 item case is untouched — only a row that actually needs the
   extra room gives any up. 200px brings Vision Pro's row down to
   ~819px, comfortably inside budget. */
.feature-card-media:has(> :nth-child(3)) .photo-item,
.feature-card-media:has(> :nth-child(3)) .video-item {
  height: 200px;
}
/* Hover language borrowed from elsewhere on the site rather than
   invented fresh: a gentle scale (same family as .feature-card's own
   translateY lift and the photo-viewer nav buttons' scale-on-hover),
   matching .photo-grid-item:hover img's existing 0.25s-ease/1.04 values
   almost exactly. Scoped to .feature-card-media specifically — .photo-item
   is also used standalone elsewhere (e.g. a flow section's plain photo
   items) where this hover isn't necessarily wanted and wasn't asked for.
   overflow: hidden lives on the stable outer element (the .photo-item
   button itself has no fixed frame of its own, unlike .photo-grid-item's
   grid cell) so the scaled-up image clips at its own rounded corners
   instead of visually spilling into the surrounding gap. No
   prefers-reduced-motion guard — checked first: none of this site's
   other hover-triggered transforms (.feature-card, .photo-grid-item,
   .link-item-icon, the photo-viewer nav buttons) guard themselves either,
   only continuous/ambient animations do (terminal cursor blink, hero
   scroll indicator) — a hover effect only plays when the user's own
   cursor triggers it, not the ambient motion prefers-reduced-motion
   targets. */
.feature-card-media .photo-item {
  border-radius: var(--radius-md);
  overflow: hidden;
}
/* height: 100%; width: auto (not the base .photo-item img's width: 100%)
   + object-fit: contain (not cover) — with the fixed-height row above,
   this is what actually stops the image being cropped: the img sizes
   itself to its own real aspect ratio at that height instead of being
   stretched/cropped to fill a box shaped by its neighbor. object-fit:
   contain is a safety net here more than the active mechanism (an img
   with only height set and width: auto already preserves its intrinsic
   aspect ratio on its own), but keeps this correct if anything upstream
   ever forces both dimensions again. Scoped to .feature-card-media
   specifically — the base .photo-item/.video-item-player rules below
   (used by the photo-grid section, hobbies' click-to-play cloth video,
   and everywhere else these components render) are untouched. */
.feature-card-media .photo-item img {
  height: 100%;
  width: auto;
  object-fit: contain;
  transition: transform 0.25s ease;
}
.feature-card-media .photo-item:hover img { transform: scale(1.04); }
/* Same sizing fix for a video's own frame — .video-item-player covers
   both the click-to-play swapped-in player (not currently used inside a
   group, but kept consistent in case one ever is) and, via cascade, the
   autoplay player below (that rule only adds position/opacity/transition,
   it doesn't redeclare object-fit itself, so this is what wins). width:
   auto is inert on the two absolutely-positioned autoplay layers
   specifically (inset: 0 mathematically forces their width to fill the
   parent regardless of what width is declared) — object-fit: contain is
   what actually does the work for those two, letterboxing instead of
   cropping within that fixed box. For a non-autoplay video in this
   context (no inset: 0 in play) width: auto genuinely shrink-wraps it to
   its real aspect ratio, same mechanism as the photo case above. */
.feature-card-media .video-item-player,
.feature-card-media .video-item-poster img,
.feature-card-media .video-item-autoplay-poster {
  height: 100%;
  width: auto;
  object-fit: contain;
}
/* Same treatment for an autoplay video's own frame — scoped to the
   autoplay case specifically (not the click-to-play .video-item-poster
   path above, which already has its own distinct hover language via the
   play button scaling up; stacking a second whole-figure scale on top of
   that would compete with it rather than fit alongside it). Both layers
   (poster and player) scale together so whichever is currently visible
   during the crossfade stays visually consistent. */
.feature-card-media .video-item-autoplay {
  border-radius: var(--radius-md);
  overflow: hidden;
}
.feature-card-media .video-item-autoplay-poster,
.feature-card-media .video-item-autoplay .video-item-player {
  transition: opacity 0.3s ease, transform 0.25s ease;
}
.feature-card-media .video-item-autoplay:hover .video-item-autoplay-poster,
.feature-card-media .video-item-autoplay:hover .video-item-player {
  transform: scale(1.04);
}
.feature-card-title {
  font-family: var(--font-display), Georgia, serif;
  font-weight: 700;
  font-size: 1.15rem;
  margin: 0;
}
.feature-card-text-only .text-item { margin: 0; }

/* Institution header row, then its project-cards stacked in a single
   column underneath — reverted from a multi-column auto-fit grid per
   explicit correction: "We shouldn't place them horizontally anymore.
   They are all their own thing in this design." Each card is its own
   full-width unit, not a grid cell alongside its siblings. */
.project-cards {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
}

/* Just a spacer between the panel and its .project-cards grid, and
   between one project's whole group and the next — the panel and each
   .feature-card already carry their own real, visible boundary, so this
   never needs one of its own. */
.project-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

/* Full-width, like a panel — a standalone block that resets the grid row,
   so whatever follows (a video, a photo) starts its own row instead of
   sitting beside the panel as if the two were equal-weight peers. */
.panel {
  grid-column: 1 / -1;
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  border-radius: var(--radius-md);
  padding: var(--space-2);
  position: sticky;
  top: 80px;
  z-index: 10;
}

.panel-header {
    display: flex;
    align-items: center;
    width: 100%;
}

.panel-header .panel-icon {
  margin-right: 10px;
  margin-bottom: 0px;
  flex-shrink: 0;
}

.panel-header .panel-title {
  margin: 0;
}

.panel-header .timeline-date {
  margin: 0 0 0 auto;
}

@media (max-width: 560px) {
    .panel {
        top: none;
        position: inherit;
    }
}
/* object-fit: contain (not the box's default stretch-to-fill) — not
   every institution logo is square (umn/epic/seagate aren't, unlike
   Apple's), so a plain fixed-box img would distort them. The white
   background is what an img element's own background-color renders
   through a transparent PNG's see-through areas, no wrapper needed —
   covers whatever's letterboxed on the constrained axis instead of
   showing the page's gradient through it. */
.panel-icon { width: 40px; height: 40px; border-radius: var(--radius-sm); margin-bottom: var(--space-3); object-fit: contain; background: #fff; }
.panel-title { font-family: var(--font-display), Georgia, serif; margin: 0 0 var(--space-2); color: rgba(28, 27, 27, 0.79); }
.panel-desc { margin: 0 0 var(--space-3); opacity: 0.85; }
.panel-detail { font-size: 0.92rem; opacity: 0.75; }
.panel .timeline-date {
    color: var(--accent-2);
    opacity: 0.8;
    font-size: 1.05em;
}
/* Institution panels (Apple, Research) are the section header for
   everything under them — same border/radius family as .feature-card
   below it, just scaled up (heavier tint, a real border instead of a
   hairline, bigger padding, a bigger/bolder title) rather than a
   different-colored block that reads as arbitrary. Reads as "the big
   card that heads this group," not a differently-styled one. Scoped to
   project-group specifically: .panel elsewhere (nova, voidforge) is a
   plain grid-cell panel among peers, not a header, and shouldn't inherit
   this weight. */
/* Glass, like .feature-card above, but a heavier dose of every knob that
   makes it glass — more opaque (68% white vs. the card's 55%), more
   blurred (20px vs. 16px), a thicker/brighter border, a bigger shadow —
   so the header-vs-card weight difference that already exists in
   padding/title/icon size carries into the material itself instead of
   the two ending up visually similar once both are just "white blur." */
.project-group .panel {
  background: color-mix(in srgb, rgba(220, 233, 216, 0.85) 54%, transparent 46%);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1.5px solid color-mix(in srgb, color(srgb 0.4984 0.56 0.5474) 25%, transparent);
  padding: var(--space-2);
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.28);
}
.project-group .panel-icon { width: 56px; height: 56px; }
.project-group .panel-title {
  font-size: 1.6rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
/* Shared "Learn more" CTA — used by panel.leaf AND timeline-item.leaf
   (same interaction, one component, not two copies). Plain colored text
   by default; the underline grows in and the arrow slides/fades in on
   hover, rather than a static arrow that's always there. Same 150-250ms
   range as the rest of catalog.css's transitions. */
.learn-more-link {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  color: var(--accent);
  font-size: 0.88rem;
  text-decoration: none;
}
.learn-more-link-text { position: relative; }
.learn-more-link-text::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.2s ease;
}
.learn-more-link:hover .learn-more-link-text::after,
.learn-more-link:focus-visible .learn-more-link-text::after { transform: scaleX(1); }
.learn-more-link-arrow {
  display: inline-block;
  transform: translateX(-6px);
  opacity: 0;
  transition: transform 0.2s ease, opacity 0.2s ease;
}
.learn-more-link:hover .learn-more-link-arrow,
.learn-more-link:focus-visible .learn-more-link-arrow { transform: translateX(0); opacity: 1; }

/* Photos embedded in a mixed section (not the dedicated photo-grid) are
   now real lightbox triggers too — same button-reset photo-grid-item
   needs, since this is also a <button> now. */
.photo-item {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
}
.photo-item img { width: 100%; border-radius: var(--radius-md); display: block; }
.photo-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Full-width, same reasoning as .panel — a video wants to be watched, not
   squeezed into an auto-fit grid cell beside unrelated text. */
/* .video-item-container carries the outer sizing (grid span, max-width)
   that used to sit directly on the figure, and lays out the caption and
   the figure as a column of two SIBLING flex items — not the caption as
   a child inside the figure, which is what used to let the caption's own
   height eat into the figure's aspect-ratio box and squish the video.
   Default (here): align-items stays the flex initial "stretch", so the
   figure stretches to the container's full width (up to max-width) and
   its height is computed FROM that width by its own inline aspect-ratio
   style (set server-side by VideoDimensionProbe — see SiteController) —
   same sizing direction this always used, just one level removed from
   the caption now. Falls back to no aspect-ratio (sized by the poster
   image itself) if a video's dimensions couldn't be read. Left-aligned
   (no auto-centering) so it sits flush with the paragraph text above it. */
.video-item-container {
  display: flex;
  flex-direction: column;
  grid-column: 1 / -1;
  margin: 0;
  max-width: 640px;
}
/* Small label reading as "this is what the video is," not a competing
   heading — same lightweight caption values .text-item already
   establishes elsewhere (0.92rem/opacity 0.75), not something new. */
.video-item-label { 
  margin: 0 0 var(--space-2); 
  font-family: var(--font-display), Georgia, serif;
  font-weight: 700;
  font-size: 1.15rem;
}
/* Just resets the <figure> element's own browser-default margin (1em/
   40px) — the actual outer sizing lives on .video-item-container above;
   real width/height come from the flex column's stretch behavior (here)
   or the feature-card-media override (below), not from anything set
   directly on the figure itself. */
.video-item { margin: 0; }
.video-item-player { width: 100%; height: 100%; border-radius: var(--radius-md); display: block; object-fit: cover; }

/* Click-to-play: a static poster image by default, a play button that
   only appears on hover/focus, crossfaded for a real <video> on click
   (see video-item.js) — not an always-visible player. */
.video-item-poster {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.video-item-poster img { width: 100%; height: 100%; border-radius: var(--radius-md); display: block; object-fit: cover; }
/* Crossfade: both poster and player animate opacity (video-item.js toggles
   these rather than swapping the DOM instantly), so the transition reads
   as one continuous moment instead of a hard cut. */
.video-item-poster,
.video-item-player { transition: opacity 0.3s ease; }
.video-item-play {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.2s ease;
}
/* Same bordered-circle language as the photo-viewer's close/prev/next
   controls (translucent fill, 30%-white border, accent border + scale on
   hover) instead of the old one-off tinted-blur disc. 68px circle — a
   little bigger than the 48px nav controls, since this is the single
   call-to-action on the whole item rather than one of three small nav
   buttons — but the triangle fills most of it (34px, ~50% of the
   circle's diameter) rather than sitting in a lot of empty padding. */
.video-item-play::before {
  content: "";
  position: absolute;
  width: 48px;
  height: 48px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--bg) 76%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.3);
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.video-item-play svg { position: relative; z-index: 1; color: #fff; width: 28px; height: 28px; }
.video-item-poster:hover .video-item-play,
.video-item-poster:focus-visible .video-item-play { opacity: 1; }
.video-item-poster:hover .video-item-play::before,
.video-item-poster:focus-visible .video-item-play::before {
  background: rgba(255, 255, 255, 0.24);
  border-color: var(--accent);
  transform: scale(1.1);
}

/* Autoplay: poster (if any) shown immediately, full-bleed; the <video>
   itself is in the markup from the start (not JS-inserted, unlike the
   click-to-play path above) so it can start loading right away — see
   video-item.leaf / video-item-autoplay.js. No play button — nothing to
   click. Crossfades in over the poster once it can play, using the same
   opacity-transition technique as the click-to-play path, just triggered
   by a load event instead of a click. */
.video-item-autoplay { position: relative; width: 100%; height: 100%; }
.video-item-autoplay-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-md);
  display: block;
  object-fit: cover;
  transition: opacity 0.3s ease;
}
.video-item-autoplay .video-item-player {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.prose-section .prose { max-width: var(--prose-width); }
.prose-section .prose h1,
.prose-section .prose h2,
.prose-section .prose h3 { font-family: var(--font-display), Georgia, serif; }
.prose-section .prose a { color: var(--accent); }
/* Default styling for a Markdown bullet list in prose — the hand-rolled
   renderer (FrontmatterMarkdown) can't emit a class on the <ul> it
   generates, so this has to target the element itself to have anywhere to
   attach to. No bullets/indent; each item reads as its own short entry via
   a colored left rule instead — extracted from gamena's original
   site-specific .tone-list into a real shared default (theme's own
   --accent, not a hardcoded color) since any prose list wants the same
   basic treatment, not just gamena's seven tones. */
.prose-section .prose ul {
  margin: 0 0 0.9rem;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.prose-section .prose ul li {
  padding-left: var(--space-3);
  border-left: 2px solid color-mix(in srgb, var(--accent) 35%, transparent);
}

/* ---- mobile ---- */
@media (max-width: 560px) {
  .container { padding: 0 var(--space-4); }
  .nav-inner { gap: var(--space-3); }
  .brand { font-size: 1rem; }
  .content-section { padding: var(--space-6) 0; }
  .content-container { grid-template-columns: 1fr; }
}

/* ---- footer ---- */
.site-footer {
  border-top: 1px solid color-mix(in srgb, var(--ink) 15%, transparent);
  margin-top: var(--space-8);
}
.footer-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  text-align: center;
  padding: var(--space-5) 0;
  font-size: 0.85rem;
  opacity: 0.7;
}
.engine-credit { font-size: 0.78rem; opacity: 0.75; }
