/* GlitchCraft Engine — catalog components that ship JS behavior
   (photo-grid/photo-viewer, timeline, calendar). Kept separate from
   tokens.css (pure design tokens) and /effects (opt-in per site). */

/* ---- eyebrow (shared by hero + section header) ---- */
.section-eyebrow { margin: 0 0 var(--space-2); }

/* ---- hero image (the trivial hero: one image, no text) ---- */
.hero-image { padding: var(--space-6) 0; text-align: center; }
.hero-image-img { max-width: min(72%, 640px); width: 100%; display: block; margin: 0 auto; border-radius: var(--radius-lg); }

/* ---- photo grid ----
   Every tile is the same shape (object-fit: cover on a fixed aspect-ratio)
   so the grid reads as one deliberate mosaic instead of ragged intrinsic
   sizes — that unevenness was the "sizes feel random" problem. The first
   tile in every row-of-6 runs bigger as a small editorial variation. */
.photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  grid-auto-rows: 200px;
  gap: var(--space-3);
}
.photo-grid-item {
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  border-radius: var(--radius-md);
  overflow: hidden;
  line-height: 0;
  height: 100%;
}
.photo-grid-item:nth-child(6n+1) {
  grid-column: span 2;
  grid-row: span 2;
}
.photo-grid-item img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.25s ease; }
.photo-grid-item:hover img { transform: scale(1.04); }
.photo-grid-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 700px) {
  .photo-grid-item:nth-child(6n+1) { grid-column: span 1; grid-row: span 1; }
}

/* ---- photo viewer (lightbox) ---- */
.photo-viewer {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.88);
}
.photo-viewer[hidden] { display: none; }
.photo-viewer-frame { margin: 0; max-width: min(92vw, 1200px); max-height: 86vh; text-align: center; }
.photo-viewer-image { max-width: 100%; max-height: 80vh; border-radius: var(--radius-sm); }
.photo-viewer-caption { color: #fff; opacity: 0.8; margin-top: var(--space-3); font-size: 0.9rem; }
.photo-viewer-close,
.photo-viewer-prev,
.photo-viewer-next {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--bg) 18%, transparent);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 999px;
  width: 48px;
  height: 48px;
  padding: 0;
  font-size: 1.7rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.photo-viewer-close { top: var(--space-5); right: var(--space-5); }
.photo-viewer-prev { left: var(--space-5); top: 50%; transform: translateY(-50%); }
.photo-viewer-next { right: var(--space-5); top: 50%; transform: translateY(-50%); }
/* Same treatment as the social links: border picks up the accent color,
   a little scale on hover. prev/next keep their centering translateY,
   just combined with the scale rather than replaced by it. */
.photo-viewer-close:hover {
  background: rgba(255, 255, 255, 0.24);
  border-color: var(--accent);
  transform: scale(1.1);
}
.photo-viewer-prev:hover,
.photo-viewer-next:hover {
  background: color-mix(in srgb, var(--bg) 24%, transparent);
  border-color: var(--accent);
  transform: translateY(-50%) scale(1.1);
}

.photo-viewer-close span,
.photo-viewer-prev span,
.photo-viewer-next span {
  position: relative;
  top: -2px;
}

/* ---- timeline ---- */
.content-container.timeline { display: flex; flex-direction: column; gap: 0; }
.timeline-item { display: flex; gap: var(--space-5); padding-bottom: var(--space-6); }
.timeline-rail { position: relative; width: 12px; flex-shrink: 0; }
.timeline-indicator {
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--accent); position: relative; z-index: 1;
}
.timeline-connector {
  position: absolute; left: 5px; top: 12px; bottom: -24px; width: 2px;
  background: color-mix(in srgb, var(--ink) 20%, transparent);
}
.timeline-item:last-child .timeline-connector { display: none; }
.timeline-date { font-family: monospace; font-size: 0.78rem; color: var(--accent); margin: 0 0 var(--space-2); }
.timeline-title { font-family: var(--font-display), Georgia, serif; margin: 0 0 var(--space-2); }
.timeline-text { margin: 0 0 var(--space-2); opacity: 0.85; }
.timeline-photo { max-width: 280px; border-radius: var(--radius-md); display: block; margin-top: var(--space-2); }
/* .learn-more-link (shared with panel.leaf) handles the "Learn more" CTA
   now — see tokens.css. No separate .timeline-link rule anymore. */

/* ---- calendar ----
   Grid-based, not a bordered table: cells separate by air (gap), not
   rules, and "today" is a filled dot on the number rather than a tinted
   cell — reads less like a spreadsheet. On desktop it's fine sharing a
   column with adjacent text (that's the intended paired layout); the
   full-width override lives in the mobile breakpoint below, where a
   7-column day grid squeezed into a narrow shared column is what's
   actually cramped. */
.calendar-header {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); margin: 0 0 var(--space-4); flex-wrap: wrap;
}
.calendar-month {
  font-family: var(--font-display), Georgia, serif;
  font-size: 1.25rem;
  margin: 0;
}
.calendar-nav { display: flex; align-items: center; gap: var(--space-2); }
.calendar-nav-btn {
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  color: var(--ink);
  text-decoration: none;
  transition: background 0.15s ease;
}
.calendar-nav-btn:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
.calendar-arrow { width: 32px; height: 32px; font-size: 1.1rem; line-height: 1; }
.calendar-today { padding: 0 var(--space-3); height: 32px; font-size: 0.8rem; }

.calendar-weekdays {
  display: grid; grid-template-columns: repeat(7, 1fr);
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em;
  opacity: 0.5; padding: 0 var(--space-1); margin-bottom: var(--space-2);
}
.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--space-1);
}
.calendar-day {
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  min-height: 76px;
  font-size: 0.82rem;
  background: color-mix(in srgb, var(--ink) 3%, transparent);
}
.calendar-day.empty { background: none; }
.calendar-day-num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px;
  font-family: monospace; font-size: 0.78rem; opacity: 0.7;
  border-radius: 50%;
  margin-bottom: var(--space-1);
}
.calendar-day-num.today { background: var(--accent); color: var(--bg); opacity: 1; font-weight: 600; }
.calendar-event {
  display: flex; align-items: center; gap: 4px;
  font-size: 0.72rem;
  padding: 1px 0;
  margin-bottom: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.calendar-event::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0;
  background: var(--accent);
}

/* ---- hero video ----
   Two <video> elements toggled by aspect ratio, same technique (and same
   breakpoint) as the real Hyde site's .video-default/.video-mobile pair.
   Full-viewport, no text overlaid on the footage — the title/subtitle
   render as their own block right below instead, so the video reads
   clean and the type is actually legible. */
.hero-video {
  position: relative; overflow: hidden;
  height: 100vh; min-height: 480px;
  display: flex; align-items: flex-end; justify-content: center;
  padding: 0;
  /* full-bleed: break out of <main class="container">'s max-width so the
     video actually spans the viewport instead of sitting in a boxed column */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}
.hero-video-bg-wrap { position: absolute; inset: 0; z-index: 0; display: flex; }
.hero-video-bg { width: 100%; height: 100%; object-fit: cover; filter: brightness(0.6); }
.hero-video-mobile { display: none; }
@media (max-aspect-ratio: 3/4) {
  .hero-video-default { display: none; }
  .hero-video-mobile { display: flex; }
}
/* Same full-bleed treatment as .hero-video above, for a site whose hero is
   one static photo rather than footage — no default/mobile pair needed
   since there's only ever the one image. Unlike .hero-video, the title
   sits ON the photo (centered, both axes) rather than as its own block
   below — the scroll indicator is pinned to the bottom independently via
   its own absolute position, not flex placement, since flex here is busy
   centering the caption instead. */
.hero-photo {
  position: relative; overflow: hidden;
  height: 100vh; min-height: 480px;
  display: flex; align-items: center; justify-content: center;
  padding: 0;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}
.hero-photo-bg-wrap { position: absolute; inset: 0; z-index: 0; display: flex; }
.hero-photo-bg { width: 100%; height: 100%; object-fit: cover; filter: brightness(0.6); }
.hero-photo-caption {
  position: relative; z-index: 1;
  color: #fff;
  text-align: center;
  padding: 0 var(--space-4);
}
.hero-photo-caption h1,
.hero-photo-caption .hero-subtitle {
  text-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
}
.hero-photo-caption .hero-subtitle { opacity: 0.9; }
.hero-photo .hero-scroll-indicator {
  position: absolute;
  left: 50%;
  bottom: var(--space-6);
  margin-bottom: 0;
  transform: translateX(-50%) rotate(90deg) scaleX(0.8) scaleY(1.2);
}
/* Plain glyph, not a button-chrome circle — matches the original site's
   .title-scroll-indicator: a rotated ">" with a drop shadow that bounces
   in place, no border/background of its own. */
.hero-scroll-indicator {
  position: relative; z-index: 1;
  margin-bottom: var(--space-6);
  padding: 0;
  border: 0;
  background: none;
  color: #fff;
  font-family: var(--font-display), Georgia, serif;
  font-size: 2.6rem;
  line-height: 1;
  cursor: pointer;
  transform: rotate(90deg) scaleX(0.8) scaleY(1.2);
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.8);
  animation: hero-scroll-fadein 1.4s ease-in 0s both, hero-scroll-bob 1s ease-in-out 1.4s infinite alternate;
}
@keyframes hero-scroll-fadein {
  0%, 40% { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes hero-scroll-bob {
  0% { translate: 0 0; }
  100% { translate: 0 0.2em; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-scroll-indicator { animation: none; opacity: 1; }
}

/* ---- floating nav (video-hero pages only, see base.leaf/floating-nav.js) ----
   Frosted glass rather than a flat panel — it's overlaying whatever
   content scrolls underneath it, so a blurred, translucent background
   reads better than a hard-edged solid one. */
body.has-floating-nav .site-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--bg) 70%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
}
body.has-floating-nav .site-nav.nav-visible {
  opacity: 1;
  pointer-events: auto;
}
.hero-video-caption {
  text-align: center;
  padding: var(--space-7) var(--space-4);
}
.hero-subtitle {
  font-family: var(--font-display), Georgia, serif;
  font-size: clamp(1.6rem, 4vw, 2.4rem);
  margin: 0;
}

/* ---- link item (icon + label, e.g. a contact row) ----
   A row of pill buttons rather than one flat tinted strip each: bordered,
   more spacing, icon lifts slightly on hover for a bit of life. */
.content-container:has(.link-item) {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.link-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
  background: color-mix(in srgb, var(--ink) 4%, transparent);
  color: var(--ink);
  text-decoration: none;
  transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease;
}
.link-item:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  transform: translateY(-2px);
}
/* These icon files are hardcoded white SVGs (built for a dark page) and
   they're <img>-less now — an inline element with the icon supplied as a
   CSS mask-image, filled with background-color, so it recolors to match
   the theme (--ink here) instead of staying stuck white. Works regardless
   of the site's own background, no badge/wrapper shape needed. */
.link-item-icon {
  display: inline-block;
  width: 16px;
  height: 16px;
  background-color: var(--ink);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
  transition: transform 0.15s ease, background-color 0.15s ease;
}
.link-item:hover .link-item-icon { transform: scale(1.12); background-color: var(--accent); }
.link-item-label { font-size: 0.92rem; font-weight: 500; }

/* ---- mobile: calendar and photo-grid tighten up ---- */
@media (max-width: 560px) {
  .calendar { grid-column: 1 / -1; } /* full-width spillover — mobile only, see note above */
  .calendar-day { min-height: 56px; padding: var(--space-1); font-size: 0.72rem; }
  .calendar-weekdays { font-size: 0.62rem; }
  .calendar-event { font-size: 0; } /* keep the dot, drop the label — no room to truncate usefully */
  .calendar-event::before { width: 6px; height: 6px; }
  .calendar-header { align-items: center; }
  .photo-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-auto-rows: 140px; }
}

/* ---- terminal window (opt-in via page.hero-terminal) ----
   A persistent scroll companion, not just a hero decoration: position:
   fixed from the start (not absolute-within-hero -> fixed-later, which
   would jump coordinate systems at the switch point), with terminal-
   scroll.js continuously updating `left` from a centered starting spot
   to a docked-left one as you scroll through the hero, then holding
   there for the rest of the page — one continuous formula, no snap.
   `top` stays constant; only horizontal position moves, per the ask.
   z-index sits below the sticky nav (40) so it never fights it. Traffic-
   light dots use the REAL macOS colors, not the theme — deliberate, not
   a missed token. .hero.has-terminal still reserves real space at the
   top of the hero so the title never risks colliding with the card,
   regardless of its exact rendered height (still unverified visually —
   see build report). */
.hero.has-terminal { padding-top: calc(var(--space-8) + 190px); }
/* Permanent left gutter for content sections once the terminal is
   docked — fixed elements don't push normal-flow content out of the
   way themselves, so without this, page content runs underneath it
   once it settles on the left. Reserved from page load (not toggled as
   it docks), matching "no layout jump when it moves into position".
   Gated by body.has-terminal-companion — the exact same
   page.hero-terminal flag the terminal itself is gated by, set in
   base.leaf — so this never applies on a page/site without one.

   Scoped to .content-section specifically, NOT <main> (which would
   also catch .hero): the terminal starts centered and only docks left
   *after* you scroll past the hero, so the hero has to stay centered
   in the full viewport exactly like a page with no terminal at all —
   only the sections that follow it, where the terminal has actually
   finished docking by the time you reach them, need the reserved
   space. .content-section is the one class every section kind
   (items/prose/photos/timeline) already renders, so this one rule
   covers all of them without needing a :not(.hero) exception.

   The 480px value (docked-left offset 24px + card width 400px + real
   breathing room) lives in exactly one place now — this custom
   property — instead of being duplicated as a second hardcoded number
   in terminal-scroll.js. The JS reads it back via getComputedStyle at
   runtime (see the script), so there's one source of truth, not two
   numbers that happen to currently agree. */
body.has-terminal-companion { --terminal-gutter: 480px; }
body.has-terminal-companion .content-section { padding-left: var(--terminal-gutter); }
.terminal-window {
  position: fixed;
  top: 90px;
  left: 50%; /* overridden per-frame by terminal-scroll.js once it runs */
  z-index: 35;
  width: min(400px, 86vw);
  pointer-events: none; /* the card itself re-enables this — see .terminal-card */
}
.terminal-card { pointer-events: auto; } /* so the traffic-light hover below actually fires */
/* Bob lives on an inner wrapper, separate from the outer element JS
   controls — a CSS animation and a JS-driven inline style can't both
   safely own the same `transform`, so horizontal (outer, JS) and
   vertical (inner, CSS) are split onto two different elements instead
   of fighting over one. */
.terminal-float {
  animation: terminal-float 6s ease-in-out infinite;
}
@keyframes terminal-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}
/* Dialed back from 55%/45% (round 2): the terminal used to float over a
   flat body background, so the glow was the only color in the area
   around it. Now that .has-gradient-bg sites (jford) put real color
   behind the terminal too, and the card itself became translucent below
   (so the glow partially shows through it, not just around it), the
   original strength risked reading as muddy oversaturation rather than a
   deliberate accent. Judgment call, unconfirmed without eyes on it. */
.terminal-glow {
  position: absolute;
  inset: -25% -15%;
  z-index: -1;
  filter: blur(50px);
  pointer-events: none;
  background:
    radial-gradient(circle at 30% 40%, color-mix(in srgb, var(--accent) 40%, transparent) 0%, transparent 60%),
    radial-gradient(circle at 70% 60%, color-mix(in srgb, var(--accent-2, var(--accent)) 35%, transparent) 0%, transparent 60%);
}
/* Glass (round 2): translucent dark + blur instead of a flat #1B1B1B —
   still a real macOS Terminal.app pastiche (dark), only the material
   changes (opaque to frosted), not the color identity toward the
   inspiration's light glass. Border is new too (real glass has an edge;
   flat opaque didn't need one). */
.terminal-card {
  background: rgba(20, 20, 22, 0.6);
  backdrop-filter: blur(24px) saturate(140%);
  -webkit-backdrop-filter: blur(24px) saturate(140%);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
}
.terminal-titlebar {
  display: flex;
  padding: 10px 14px;
  background: rgba(12, 30, 32, 0.28);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
/* Group hover, not per-dot — real macOS reveals all three glyphs at
   once when you hover the button cluster, not one at a time. */
.terminal-traffic-lights { display: flex; gap: 8px; }
.terminal-dot { position: relative; width: 12px; height: 12px; border-radius: 50%; }
.terminal-dot-red { background: #EE6A5F; }
.terminal-dot-yellow { background: #F4BF4F; }
.terminal-dot-green { background: #61C454; }
/* Glyphs drawn with pseudo-elements (bars, rotated for the ×) — no new
   SVG assets. Each glyph is a darkened shade of its OWN dot's color
   (color-mix toward black), which is what real macOS does and reads
   as real contrast against any of the three base colors without
   needing a different rule per dot. */
.terminal-dot::before,
.terminal-dot::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  opacity: 0;
  transition: opacity 0.12s ease;
}
.terminal-traffic-lights:hover .terminal-dot::before,
.terminal-traffic-lights:hover .terminal-dot::after { opacity: 1; }
.terminal-dot-red::before,
.terminal-dot-red::after {
  width: 7px;
  height: 1.4px;
  background: color-mix(in srgb, #EE6A5F 35%, black);
}
.terminal-dot-red::before { transform: translate(-50%, -50%) rotate(45deg); }
.terminal-dot-red::after { transform: translate(-50%, -50%) rotate(-45deg); }
.terminal-dot-yellow::before {
  width: 7px;
  height: 1.4px;
  background: color-mix(in srgb, #F4BF4F 35%, black);
  transform: translate(-50%, -50%);
}
.terminal-dot-green::before,
.terminal-dot-green::after {
  background: color-mix(in srgb, #61C454 35%, black);
}
.terminal-dot-green::before {
  width: 7px;
  height: 1.4px;
  transform: translate(-50%, -50%);
}
.terminal-dot-green::after {
  width: 1.4px;
  height: 7px;
  transform: translate(-50%, -50%);
}
.terminal-body {
  padding: 16px 18px 20px;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.82rem;
  line-height: 1.8;
  color: #E8E8E8;
  text-align: left;
}
.terminal-row { white-space: nowrap; }
.terminal-prompt { color: #61C454; margin-right: 8px; }
.terminal-typed { display: inline; }
.terminal-output { color: #A9A9A9; margin: 2px 0 0; opacity: 0; transition: opacity 0.2s ease; }
.terminal-cursor {
  display: inline-block;
  width: 0.55ch;
  height: 1em;
  background: #E8E8E8;
  vertical-align: text-bottom;
  margin-left: 2px;
  animation: terminal-cursor-blink 1s steps(1, end) infinite;
}
@keyframes terminal-cursor-blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}

/* Typed-command history (round 3) — appended to, never overwritten, kept
   under its own scroll so a long session can't grow the card without
   bound. Collapses to zero height while empty (:empty) so a page that
   loads with no typed commands yet doesn't pay for the region at all —
   only the always-present input row below adds height in that case. */
.terminal-log {
  max-height: 120px;
  overflow-y: auto;
  margin-top: 8px;
}
.terminal-log:empty { margin-top: 0; }
.terminal-log-command,
.terminal-log-output,
.terminal-log-error { margin: 0 0 4px; }
.terminal-log-command { color: #E8E8E8; }
.terminal-log-output { color: #A9A9A9; }
.terminal-log-error { color: #EE6A5F; }
.terminal-input-row {
  display: flex;
  align-items: center;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.terminal-input-row .terminal-prompt { margin-right: 8px; }
.terminal-input {
  flex: 1;
  min-width: 0;
  background: none;
  border: 0;
  outline: 0;
  color: #E8E8E8;
  font: inherit;
  padding: 0;
}
@media (prefers-reduced-motion: reduce) {
  /* The bob and cursor blink stop; terminal-scroll.js also checks this
     itself and skips the per-character typing effect (instant text swap
     instead) and the scroll-tied horizontal movement (see report). */
  .terminal-float, .terminal-cursor { animation: none; }
  .terminal-cursor { opacity: 1; }
}
@media (max-width: 700px) {
  /* No room for the horizontal dock-left motion at this width, so it's
     dropped entirely — terminal-scroll.js checks this same breakpoint
     and stops writing an inline `left` at all here, handing positioning
     fully back to these rules instead of fighting them. It's pinned at
     the top of the viewport (below the sticky nav) rather than hidden;
     the content-swap-on-scroll (IntersectionObserver) keeps running
     unchanged — nothing about that behavior was scroll-position/
     horizontal-motion dependent, so there's no reason to turn it off
     here too. */
  .terminal-window {
    display: block;
    top: 78px; /* round 5, explicit value requested — 66px -> 34px (round 4) sat too close under the sticky nav */
    left: var(--space-4);
    right: var(--space-4);
    width: auto;
  }
  /* Unlike the desktop gutter above, this one deliberately DOES apply to
     the hero too, via <main> rather than .content-section — on mobile
     the terminal never docks anywhere, it's simply pinned at the top of
     the viewport for the entire page including the hero, so the hero
     needs to clear it just as much as everything after it does. That's
     a genuinely different situation from the desktop case, not the same
     bug. 238px ≈ the 88px top offset + the mobile card's real height +
     a bit of clearance below it; overrides the hero's own larger
     desktop-only reserve (that 190px was sized for the terminal's old
     centered starting position inside the hero, which doesn't apply
     once it's simply pinned page-wide) rather than stacking on top of
     it. The desktop left gutter has nothing to do at this width (the
     card spans full-width, not docked to one side) — reset on
     .content-section specifically, matching where it's actually
     applied now instead of the no-op it would be on <main>. */
  .hero.has-terminal { padding-top: var(--space-8); }
  /* Tracks the terminal's own top offset + real card height + a bit of
     clearance below it — shifts 1:1 with the top value above (round 5:
     188px -> 232px alongside 34px -> 78px). */
  body.has-terminal-companion main { padding-top: 232px; }
  body.has-terminal-companion .content-section { padding-left: 0; }
  /* The typed-command input doesn't earn its keep on a phone screen — no
     physical keyboard means reaching it is a real ask, and the ambient
     status line above (whoami / cat about.md / ...) already carries the
     "terminal" feel on its own. Hidden outright rather than left visible
     but inert: terminal-scroll.js's Enter-key handler is still wired to
     it, just never triggered since a hidden input can't take focus. */
  .terminal-input-row { display: none; }
  /* .terminal-glow's -15% horizontal inset (see its own rule above) is a
     deliberate blur-radius bleed past the card's own edges — harmless on
     desktop where the card floats with real room around it, but on
     mobile the card already sits close to the viewport edges (left/right:
     var(--space-4) above), so that same 15%-of-card-width bleed pokes
     past the actual viewport and forces horizontal scroll. Dropped to 0
     horizontally here (vertical bleed stays, it never caused this) — the
     50px blur still softens the edge plenty without any extra spread. */
  .terminal-glow { inset: -25% 0; }
}
