/* 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; }
}
/* 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: 88px; /* more clearance below the sticky nav than the old 60px gave it */
    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); }
  /* 238px -> 280px (round 3): the new always-visible input row adds real
     height to the card (the typed-history log above it stays collapsed
     to zero until something's actually been typed, so it isn't counted
     here). +42px is an estimate from the row's own padding/border/line-
     height, not a measured value — unconfirmed without eyes on it. */
  body.has-terminal-companion main { padding-top: 280px; }
  body.has-terminal-companion .content-section { padding-left: 0; }
}
