/* craft.css — motion, graphics and interaction layer.
   Techniques ported from vercel-labs/vgpu (vgpu.sh): the prism hero, the
   tapered hairline rule, the stacked-crossfade tab line, mono frame labels,
   the source -> outputs connector tree, and the file-tab panel. Rebuilt in
   vanilla CSS on this site's own tokens (light default, coral accent, serif
   headings) rather than Geist-on-black, so the craft arrives without the
   borrowed identity.

   Layered ON TOP of styles.css. Nothing here is required for the page to
   read: every component degrades to a static, legible baseline with no JS,
   no WebGL, or reduced motion set. */

:root {
  /* Mono micro-label: 10px, wide tracking, uppercase. vgpu uses this on every
     panel header and it is most of why their frames read as instruments. */
  --label-size: 10px;
  --label-track: 0.16em;
  /* Warm-weighted spectrum. A true rainbow reads as someone else's brand;
     leaning the ramp into coral/amber keeps it this site's. */
  --spectrum: linear-gradient(
    90deg,
    #ff4f2f 0%,
    #ff8a3d 16%,
    #ffc94d 32%,
    #8fe06a 50%,
    #46c7f0 68%,
    #7d8cf5 84%,
    #c07df5 100%
  );
  --spectrum-warm: linear-gradient(
    90deg,
    #ff5a33 0%,
    #ff8a3d 26%,
    #ffc24d 52%,
    #ffa864 74%,
    #ff6a42 100%
  );
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ---------------------------------------------------------------- mono label */

.flabel {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  height: 34px;
  padding: 0 12px;
  border-bottom: 1px solid var(--border);
  font-family: var(--mono);
  font-size: var(--label-size);
  font-weight: 500;
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: var(--muted);
}

.flabel b {
  font-weight: 600;
  color: var(--heading);
  white-space: nowrap;
}

/* The right-hand detail is the first thing to give: a two-word detail that
   wraps makes one card's header taller than its neighbours and the row of
   frames stops reading as a row. */
.flabel > span {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

/* --------------------------------------------------- tapered hairline rule */

/* A hard 1px border reads as a UI seam pinned over an image; fading both ends
   lets the line sit inside it. Gradient, not a border, because a border
   cannot taper. Peak moves toward the left on wide screens so the rule tracks
   a left-aligned column. */
.hairline {
  height: 1px;
  width: 100%;
  --peak: 50%;
  background: linear-gradient(
    to right,
    transparent,
    currentColor var(--peak),
    transparent
  );
  opacity: 0.45;
}

@media (min-width: 1100px) {
  .hairline { --peak: 22%; }
}

/* ------------------------------------------------------------- prism hero */

.banner-hero > .banner-body { position: relative; z-index: 2; }
.banner-hero > .banner-credit { position: absolute; z-index: 2; }

/* z-index 1, not -1: a negative-index child would be painted under the
   ::after scrim, which would eat most of the beam. 1 puts it above the scrim
   and below the copy (z-index 2). */
.prism-layer {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* screen keeps the painting underneath readable: the beam adds light, it
     never paints over the canvas. */
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 1.1s var(--ease-out);
}

.prism-layer.lit { opacity: 1; }
.prism-layer canvas { display: block; width: 100%; height: 100%; }

/* The beam's entry point gets a soft bloom in CSS rather than a second render
   pass: cheaper, and it survives when WebGL is unavailable. */
.banner-hero::before {
  content: "";
  position: absolute;
  left: -6%;
  top: 46%;
  width: 42%;
  height: 22%;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(
    60% 50% at 0% 50%,
    rgba(255, 214, 170, 0.28),
    rgba(255, 140, 90, 0.1) 45%,
    transparent 72%
  );
  opacity: 0;
  transition: opacity 1.4s var(--ease-out);
}

.banner-hero.lit::before { opacity: 1; }

/* The prism needs a dark stage. A screen-blended beam over a bright, high-key
   landscape washes out to nothing, which is exactly why vgpu's prism sits on
   pure black. Rather than lose the painting, the right half of the hero is
   dimmed: the copy keeps the lit mountains on the left, and the fan opens
   across a ground dark enough to carry colour. */
.banner-hero .prism-stage {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.2s var(--ease-out);
  background: linear-gradient(
    100deg,
    transparent 26%,
    rgba(6, 8, 14, 0.30) 55%,
    rgba(4, 6, 11, 0.58) 100%
  );
}

.banner-hero.lit .prism-stage { opacity: 1; }

/* Spectrum sweep on the accent word. Slow enough to be noticed only on a
   second look, which is the point. */
.banner-title .c,
.intro-title .c {
  background: var(--spectrum-warm);
  background-size: 260% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: spectrum-drift 18s linear infinite;
}

@keyframes spectrum-drift {
  0% { background-position: 0% 50%; }
  100% { background-position: 260% 50%; }
}

/* ------------------------------------------------------ hero tab line */

.htabs { display: flex; flex-direction: column; margin-top: 20px; }

.htabs-list {
  display: flex;
  align-items: center;
  font-size: 14px;
  line-height: 1;
  color: #fff;
}

.htabs-list .sep { padding: 0 8px; opacity: 0.35; }

.htabs-list button {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  opacity: 0.5;
  transition: opacity 160ms var(--ease-out);
}

.htabs-list button:hover { opacity: 0.8; }
.htabs-list button[aria-selected="true"] { opacity: 1; }

.htabs .hairline { color: #fff; margin: 18px 0 8px; }

/* Every line is rendered and stacked in ONE grid cell, so the block is as
   tall as the longest line and nothing below it can move during a switch. */
.htabs-lines {
  display: grid;
  text-align: left;
  font-size: 14px;
  line-height: 1.6;
  color: rgba(255, 255, 255, 0.9);
  max-width: 46ch;
}

/* The trailing link is the call to action for the line; wrapping "Read the
   writing" across two rows turns it into debris. */
.htabs-lines a {
  white-space: nowrap;
  color: #fff;
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.45);
  transition: border-color 160ms var(--ease-out);
}

.htabs-lines a:hover { border-bottom-color: #fff; }

.htabs-lines > span {
  grid-column: 1;
  grid-row: 1;
  /* Idle lines are parked BELOW the line with no transition, so the next one
     always rises from below and re-parking after a switch costs nothing. */
  transform: translateY(6px);
  opacity: 0;
  user-select: none;
}

.htabs-lines > span[data-state="active"] {
  transform: none;
  opacity: 1;
  user-select: auto;
  transition: transform 200ms var(--ease-out), opacity 200ms var(--ease-out);
}

/* Leaving lifts UP while arriving rises from below: direction is what makes
   it read as a handover instead of a dissolve. Slightly faster out than in. */
.htabs-lines > span[data-state="leaving"] {
  transform: translateY(-6px);
  opacity: 0;
  transition: transform 150ms var(--ease-out), opacity 150ms var(--ease-out);
}

.htabs-lines code {
  font-family: var(--mono);
  font-size: 13px;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 5px;
  padding: 2px 6px;
}

.htabs-copy {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
  cursor: pointer;
  opacity: 0;
  transition: opacity 180ms var(--ease-out), color 180ms var(--ease-out);
}

.htabs:hover .htabs-copy,
.htabs-copy:focus-visible { opacity: 1; }
.htabs-copy:hover { color: #fff; }
.htabs-copy[hidden] { display: none; }
.htabs-copy.done { opacity: 1; color: #9be8a4; }

/* ------------------------------------------------------------------ panel */

.panel {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  overflow: hidden;
  margin: 26px 0;
}

.panel-body { padding: 16px 18px; }

.panel pre {
  margin: 0;
  padding: 16px 18px;
  overflow-x: auto;
  background: var(--code-bg);
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.75;
  color: var(--code-fg);
}

/* File tabs. The active tab is marked by a 1px rule at its own foot rather
   than a filled chip, so the strip stays as quiet as the panel it labels. */
.ptabs {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
  scrollbar-width: none;
}

.ptabs::-webkit-scrollbar { display: none; }

.ptabs button {
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 1px solid transparent;
  padding: 11px 14px;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--muted);
  white-space: nowrap;
  cursor: pointer;
  transition: color 160ms var(--ease-out), background 160ms var(--ease-out);
}

.ptabs button:hover { color: var(--heading); }

.ptabs button[aria-selected="true"] {
  color: var(--heading);
  background: var(--surface-muted);
  border-bottom-color: var(--accent);
}

.panel-pane { display: none; }
.panel-pane[data-active] { display: block; }

/* Split panel: source on the left, what it produced on the right. */
.panel-split {
  display: grid;
  grid-template-columns: 1.35fr 1fr;
}

.panel-split > * + * { border-left: 1px solid var(--border); }

@media (max-width: 720px) {
  .panel-split { grid-template-columns: 1fr; }
  .panel-split > * + * { border-left: 0; border-top: 1px solid var(--border); }
}

/* A highlighted source line, for pointing at the one line that matters. */
.panel .hl {
  display: block;
  margin: 0 -18px;
  padding: 0 18px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow: inset 2px 0 0 var(--accent);
}

/* ------------------------------------------------------- connector fan-out */

/* vgpu sets the whole fan-out on a recessed full-bleed band, which is what
   separates "a diagram in the page" from "a figure the page is built around". */
.fanout {
  margin: 56px 0 12px;
  padding: 40px 24px 44px;
  border-radius: 16px;
  background: var(--surface-muted);
  border: 1px solid var(--border);
}

.fanout > .section-title,
.fanout > .tile-body {
  text-align: center;
  max-width: 62ch;
  margin-left: auto;
  margin-right: auto;
}

.fanout > .tile-body { margin-bottom: 30px; }

.fanout-source { max-width: 460px; margin: 0 auto; }

/* Geometry: one stem down from the source, one spine across, one drop into
   each output, with a dot at each junction. Plain hairlines, so it costs no SVG.

   The drops share the OUTPUT grid rather than sitting at fixed percentages of
   the row. Percentages look right until you remember the cards are in a gapped
   grid, at which point every drop is a few pixels off its card's centre and
   drifts further as the viewport changes. Same grid, same gap, same track
   widths: the drop is at 50% of its own column, which is exactly the card. */
.fanout-wires {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
  height: 74px;
  color: var(--border);
}

.fanout-wires i {
  position: absolute;
  background: currentColor;
  display: block;
}

.fanout-stem { left: 50%; top: 0; width: 1px; height: 50%; }
.fanout-spine { top: 50%; height: 1px; }

/* One cell per output; the drop and dot are positioned inside it. */
.fanout-leg { position: relative; }
.fanout-leg .fanout-drop { left: 50%; top: 50%; bottom: 0; width: 1px; }
.fanout-leg .fanout-dot {
  left: 50%;
  top: 50%;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background: currentColor;
}

/* The pulse is the one thing vgpu's static tree does not do: a short bright
   segment travels the spine, so the diagram reads as flow rather than as a
   bracket. It rides on top of the hairline and inherits the spectrum. */
.fanout-pulse {
  position: absolute;
  top: calc(50% - 1px);
  height: 3px;
  width: 88px;
  border-radius: 2px;
  background: var(--spectrum);
  filter: blur(1px);
  opacity: 0;
  pointer-events: none;
}

.fanout.running .fanout-pulse {
  animation: fanout-travel 3.6s var(--ease-out) infinite;
}

@keyframes fanout-travel {
  0%   { left: 50%; opacity: 0; }
  12%  { opacity: 0.95; }
  50%  { left: 12.5%; opacity: 0.95; }
  62%  { left: 12.5%; opacity: 0; }
  63%  { left: 50%; opacity: 0; }
  75%  { opacity: 0.95; }
  99%  { left: calc(87.5% - 88px); opacity: 0.95; }
  100% { left: calc(87.5% - 88px); opacity: 0; }
}

.fanout-outs {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
}

.fanout-out {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  overflow: hidden;
}

.fanout-out .flabel { height: 30px; padding: 0 10px; }

.fanout-out p {
  margin: 0;
  padding: 12px 12px 14px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--muted);
}

@media (max-width: 900px) {
  .fanout-outs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Two cards per row leaves no space for the detail; the name carries it. */
  .fanout-out .flabel > span { display: none; }
  .fanout-wires { height: 40px; display: block; }
  .fanout-spine, .fanout-leg, .fanout-pulse { display: none; }
  .fanout-stem { height: 100%; }
}

/* ------------------------------------------------------ cursor spotlight */

/* A tile lifts a faint warm pool under the cursor. --mx/--my are written by
   craft.js; with no JS the pool never appears and the tile is unchanged. */
.tile, .feed-item, .panel, .fanout-out { position: relative; }

.spot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity 260ms var(--ease-out);
  background: radial-gradient(
    240px circle at var(--mx, 50%) var(--my, 50%),
    color-mix(in srgb, var(--accent) 14%, transparent),
    transparent 70%
  );
}

.spot:hover::after { opacity: 1; }

/* ------------------------------------------------------- reading progress */

.readbar {
  position: fixed;
  left: 0;
  top: 0;
  height: 2px;
  width: 100%;
  z-index: 40;
  transform-origin: 0 50%;
  transform: scaleX(0);
  background: var(--spectrum);
  pointer-events: none;
}

/* ------------------------------------------------------------ ⌘K palette */

.kbtn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 28px;
  padding: 0 8px 0 10px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface);
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
  cursor: pointer;
  transition: border-color 160ms var(--ease-out), color 160ms var(--ease-out);
}

.kbtn:hover { color: var(--heading); border-color: var(--accent); }

.kbtn kbd {
  font-family: var(--mono);
  font-size: 10px;
  padding: 1px 4px;
  border-radius: 4px;
  background: var(--surface-muted);
  border: 1px solid var(--border);
}

.pal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  align-items: start;
  justify-items: center;
  padding: 12vh 16px 16px;
  background: color-mix(in srgb, var(--bg) 62%, transparent);
  backdrop-filter: blur(8px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms var(--ease-out);
}

.pal.open { opacity: 1; pointer-events: auto; }

.pal-box {
  width: 100%;
  max-width: 560px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: var(--shadow);
  overflow: hidden;
  transform: translateY(-10px) scale(0.985);
  transition: transform 200ms var(--ease-out);
}

.pal.open .pal-box { transform: none; }

.pal-input {
  width: 100%;
  border: 0;
  border-bottom: 1px solid var(--border);
  background: none;
  padding: 15px 18px;
  font-family: var(--sans);
  font-size: 15px;
  color: var(--heading);
}

.pal-input:focus { outline: none; }
.pal-input::placeholder { color: var(--dim); }

.pal-list { max-height: 46vh; overflow-y: auto; padding: 6px; }

.pal-list a {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 9px 12px;
  border-radius: 8px;
  text-decoration: none;
  color: var(--text);
}

.pal-list a[data-on] { background: var(--surface-muted); }
.pal-list a[data-on] .pal-title { color: var(--accent-strong); }

.pal-title {
  font-size: 14px;
  font-weight: 500;
  color: var(--heading);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pal-meta {
  margin-left: auto;
  flex: none;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: var(--dim);
}

.pal-empty, .pal-foot {
  padding: 14px 18px;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
}

.pal-foot { border-top: 1px solid var(--border); display: flex; gap: 16px; }

/* --------------------------------------------------------- ASCII art scrub */

/* The post art already cycles frames. Hovering hands the reader the
   transport: the frame follows the pointer across the block. */
.post-ascii { cursor: ew-resize; }

/* --------------------------------------------------------------- staggered */

/* styles.css reveals .feed-item/.bento-card/.home-section on scroll. --i is
   set by craft.js so siblings arrive in sequence instead of as a slab. */
.feed-item, .tile { transition-delay: calc(var(--i, 0) * 45ms); }

@media (prefers-reduced-motion: reduce) {
  .banner-title .c, .intro-title .c { animation: none; }
  .fanout.running .fanout-pulse { animation: none; opacity: 0.9; left: 50%; }
  .prism-layer, .banner-hero::before { transition: none; }
  .htabs-lines > span { transition: none !important; }
  .feed-item, .tile { transition-delay: 0s; }
  .pal, .pal-box { transition: none; }
}

/* ------------------------------------------------------------- responsive */

@media (max-width: 760px) {
  /* The nav is already tight at this width; the search chip drops to just its
     shortcut hint so it costs a chip instead of a row. */
  .kbtn span { display: none; }
  .kbtn { padding: 0 8px; }

  .htabs { margin-top: 16px; }
  .htabs-list { font-size: 13px; }
  .htabs-lines { font-size: 13px; max-width: none; }
  .htabs .hairline { margin: 14px 0 8px; }

  .fanout { padding: 28px 16px 30px; }
  .fanout-source { max-width: none; }
}

@media (max-width: 620px) {
  /* One flex row cannot hold the brand, five links, the search chip and the
     theme toggle at phone widths: the brand was wrapping to two lines and
     colliding with the nav. Two rows, brand above, nav below, nav scrolls if
     it still runs long. */
  .topbar {
    flex-wrap: wrap;
    row-gap: 12px;
    padding: 20px 20px 0;
  }
  .topbar-brand { flex: 1 0 auto; }
  .topbar-name { white-space: nowrap; }
  .topbar-nav {
    flex: 1 0 100%;
    align-items: center;
    justify-content: flex-start;
    overflow-x: auto;
    scrollbar-width: none;
    padding-bottom: 2px;
  }
  .topbar-nav::-webkit-scrollbar { display: none; }
  .topbar-nav a { white-space: nowrap; }

  /* Four tabs plus separators do not fit one line under 620px, and a wrapped
     tablist reads as broken. Scroll it instead. */
  .htabs-list {
    overflow-x: auto;
    scrollbar-width: none;
    white-space: nowrap;
  }
  .htabs-list::-webkit-scrollbar { display: none; }
  .pal { padding: 8vh 12px 12px; }
}
