/* ROSINT hero panels — the panel language of the threat-intelligence film.
 *
 * Every frame is a near-black backing, a dim hairline and four bright corner
 * brackets; panels add a bold mono heading with a full-width rule under it.
 * Content is real structure (a chip pinout feeding a dump, hex that encodes
 * the disassembly beside it, sourced strings, outbound arcs), never random
 * HUD filler. Each panel has a slot for its projects, examples or press.
 */

:root {
  /* Two greens: the bright brand green (--matrix) marks what you can act on
   * (project entries, buttons, links); decoration (frames, rules, the
   * card visuals) uses the muted --deco. Text keeps its own colours. */
  --deco: #4e6f3e;
  --deco-dim: rgb(78 111 62 / 0.5);
  --deco-glow: rgb(78 111 62 / 0.08);
  --act-border: rgb(81 255 13 / 0.7);
  --act-glow: rgb(81 255 13 / 0.7);
  --hair: rgb(78 111 62 / 0.5);
  --dim: rgb(81 255 13 / 0.45);
  --grey: #b8c8b8;
  --glow: rgb(81 255 13 / 0.35);
  --panel-bg: rgb(2 8 2 / 0.88);
  /* Decoration (card visuals, map backdrop, status readouts, the detail
   * scenes) recedes to this share of its painted strength, so the entries
   * and links are the brightest things on the page. */
  --deco-opacity: 0.5;
}

/* ---- decoration recedes --------------------------------------------------- */
.panel__viz,
.panel > .panel__bg,
.panel__status,
.detail__anim {
  opacity: var(--deco-opacity);
}

.detail__anim {
  --deco-opacity: 0.65;
}

/* ---- frame: backing, hairline, corner brackets ---------------------------- */
.frame {
  --arm: 1.35rem;
  --bar: 3px;
  --bracket: var(--deco);
  --bracket-glow: var(--deco-glow);
  position: relative;
  background: var(--panel-bg);
  border: 1px solid var(--hair);
}

.frame::before {
  content: "";
  position: absolute;
  inset: -1px;
  pointer-events: none;
  background:
    linear-gradient(var(--bracket) 0 0) 0 0 / var(--arm) var(--bar),
    linear-gradient(var(--bracket) 0 0) 0 0 / var(--bar) var(--arm),
    linear-gradient(var(--bracket) 0 0) 100% 0 / var(--arm) var(--bar),
    linear-gradient(var(--bracket) 0 0) 100% 0 / var(--bar) var(--arm),
    linear-gradient(var(--bracket) 0 0) 0 100% / var(--arm) var(--bar),
    linear-gradient(var(--bracket) 0 0) 0 100% / var(--bar) var(--arm),
    linear-gradient(var(--bracket) 0 0) 100% 100% / var(--arm) var(--bar),
    linear-gradient(var(--bracket) 0 0) 100% 100% / var(--bar) var(--arm);
  background-repeat: no-repeat;
  filter: drop-shadow(0 0 3px var(--bracket-glow));
}

/* ---- panel ------------------------------------------------------------------
 * A size container: its body goes two-column (visual | slot) when the panel
 * itself is wide enough, whatever the page layout around it. */
/* Every panel has the same rhythm, so cards side by side line up:
 *   title / caption (two lines reserved) / visual band / slot / status.
 * Wide panels give the visual a fixed-height band laid out across it;
 * narrow ones stack it. ACTIVISM carries its visual as the card background
 * and keeps the band's height as open space over the map. */
.panel {
  /* The band tracks viewport height: the desktop hero must fit one screen
   * (no page scroll from 1366x768 up), so short screens get a lower band. */
  --viz-h: clamp(3.6rem, 7.5vh, 7rem);
  container-type: inline-size;
  display: flex;
  flex-direction: column;
  padding: 0.7rem 1rem 0.6rem;
  font-size: 0.74rem;
  line-height: 1.5;
}

.panel > * {
  position: relative;
  z-index: 1;
}

.panel > .panel__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  pointer-events: none;
}

.panel__title {
  font-size: clamp(0.95rem, 1.2vw, 1.2rem);
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1.15;
  text-transform: uppercase;
  text-shadow: 0 0 8px var(--glow);
  padding-bottom: 0.35rem;
  margin-bottom: 0.55rem;
  border-bottom: 2px solid var(--deco);
}

/* One line of caption in every panel keeps their bands level. */
.panel__caption {
  color: var(--grey);
  margin: 0.35rem 0 0.55rem;
  font-size: 0.82rem;
  line-height: 1.35;
  min-height: 1.35em;
}

/* The project list comes first; the visual takes whatever height is left
 * in the card after it (see the desktop rules near the end). */
.panel__body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.panel__body > .slot {
  order: 1;
}

.panel__body > .panel__viz {
  order: 2;
}

.fw-map,
.re-asm {
  font-size: 0.72rem;
  line-height: 1.32;
}

.panel__viz svg {
  display: block;
  width: 100%;
  max-width: 22rem;
  height: auto;
  margin-inline: auto;
  overflow: visible;
}

/* Wide panels: the visual gets one fixed-height band, laid out across. */
@container (min-width: 20rem) {
  .panel__viz {
    height: var(--viz-h);
  }

  /* The SVG box fills the band; its viewBox scales to fit inside it. */
  .panel__viz svg {
    width: 100%;
    max-width: 100%;
    height: 100%;
  }

  .panel--fw .panel__viz {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: minmax(0, 1fr);
    align-items: center;
    gap: 1rem;
  }

  .panel--fw .fw-map {
    margin-top: 0;
  }

  .panel--re .panel__viz {
    display: grid;
    align-content: center;
  }
}

.panel__status {
  margin-top: auto;
  padding-top: 0.3rem;
  font-size: 0.74rem;
  border-top: 1px solid var(--hair);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The status rule keeps clear of the slot above it. */
.panel__body {
  margin-bottom: 0.45rem;
}

/* ---- slot: the panel's projects, examples or press reports ------------------ */
.slot__label {
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 0.3rem;
}

/* Project entries sit two-up where they fit (one-up otherwise); press
 * reports are full-width rows. */
.slot__list {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 9.5rem), 1fr));
  gap: 0.45rem;
}

.slot--press .slot__list {
  grid-template-columns: minmax(0, 1fr);
}

.slot__list > li {
  display: grid;
}

/* A project selector: logo (square slot, object-fit keeps every logo's own
 * aspect ratio without clipping) and name. The description lives in the
 * centre window it opens (projects.js). */
.entry {
  --arm: 0.6rem;
  --bar: 2px;
  --bracket: var(--matrix);
  --bracket-glow: var(--act-glow);
  --mark: clamp(2.4rem, 4.6vh, 3.2rem);
  border-color: var(--act-border);
  display: grid;
  grid-template-columns: var(--mark) minmax(0, 1fr);
  align-items: center;
  column-gap: 0.65rem;
  row-gap: 0.05rem;
  padding: 0.4rem 0.55rem;
  color: var(--matrix);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: border-color 120ms linear, background-color 120ms linear;
}

.entry__mark {
  width: var(--mark);
  height: var(--mark);
  object-fit: contain;
}

/* Projects with no logo asset get a two-letter mark of the same size. */
.entry__mark--text {
  display: grid;
  place-items: center;
  border: 1px solid currentColor;
  border-radius: 50%;
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.entry__name {
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  line-height: 1.2;
  text-transform: uppercase;
  overflow-wrap: anywhere;
}

.entry__meta {
  font-size: 0.74rem;
  color: var(--grey);
  line-height: 1.3;
}

a.entry:hover,
a.entry:focus-visible,
a.entry[aria-expanded="true"] {
  border-color: var(--matrix);
  background: rgb(81 255 13 / 0.2);
  box-shadow: 0 0 10px var(--act-glow);
  outline: none;
}

/* Press reports: a short, source-linked title over source · date, left
 * aligned in a full-width row. They sit over the ACTIVISM map, so their
 * backing is a little more open. */
.entry--press {
  grid-template-columns: minmax(0, 1fr);
  row-gap: 0.1rem;
  padding: 0.4rem 0.6rem;
  background: rgb(2 8 2 / 0.8);
}

.entry--press .entry__name {
  font-size: 0.95rem;
  letter-spacing: 0.01em;
  text-transform: none;
}

/* Phones (style.css, one column): tiles still sit two-up at ~165px each, where
 * a nine-letter name (PIRATETOK, LAWKEEPER) breaks mid-word above 0.86rem. */
@media (max-width: 40rem) {
  .entry__name,
  .entry--press .entry__name {
    font-size: 0.86rem;
  }
}

/* Crate names are identifiers: shown exactly as published. */
.entry__name--crate {
  text-transform: none;
  letter-spacing: 0.01em;
}

/* A contact link (Firmware): label over address, no mark, full width. */
.entry--contact {
  grid-template-columns: minmax(0, 1fr);
  row-gap: 0.1rem;
}

/* ---- project details: a terminal window in the centre ----------------------
 * Opened from a project tile in place of the eagle (projects.js); Back or
 * Escape returns to the eagle and to the tile that opened it. */
.home[hidden],
.detail[hidden],
.detail__body[hidden] {
  display: none;
}

.detail {
  --arm: 1.6rem;
  width: min(100%, 40rem);
  display: flex;
  flex-direction: column;
  /* 97% opaque: the eagle stays faintly visible behind the window. */
  background: rgb(2 8 2 / 0.97);
}

.detail__bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  padding: 0.55rem 0.6rem 0.55rem 1.1rem;
  border-bottom: 1px solid var(--hair);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
}

.detail__path {
  color: var(--dim);
  overflow-wrap: anywhere;
}

.detail__slug {
  color: var(--matrix);
}

.detail__slug::after {
  content: "_";
  margin-left: 0.1em;
}

/* Close: an icon-only ✕ (its name comes from aria-label). */
.detail__close {
  appearance: none;
  flex: none;
  display: grid;
  place-items: center;
  width: 2.1rem;
  height: 2.1rem;
  border: 1px solid var(--act-border);
  background: transparent;
  color: var(--matrix);
  font: inherit;
  font-size: 1.2rem;
  line-height: 1;
  padding: 0;
  cursor: pointer;
  transition: border-color 120ms linear, background-color 120ms linear;
}

.detail__close:hover,
.detail__close:focus-visible {
  border-color: var(--matrix);
  background: rgb(81 255 13 / 0.12);
  outline: none;
}

/* One column: logo and title centred on top, then the text, the project's
 * animation in whatever height is left, and the two buttons at the bottom. */
.detail__body {
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
  padding: 1.2rem 1.4rem 1.4rem;
}

.detail__mark,
.detail__title {
  align-self: center;
  text-align: center;
}

.detail__anim {
  flex: none;
  height: 11rem;
  display: grid;
  place-items: center;
}

.detail__anim svg {
  display: block;
  width: 100%;
  height: 100%;
}

.sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* PirateTok's library list: language and package, each linking to its repo. */
.detail__label {
  font-size: 0.74rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
}

.detail__libs {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 10.5rem), 1fr));
  gap: 0.45rem 1rem;
  font-size: 0.84rem;
}

/* Icon beside two lines: the language, then the package name (which may
 * wrap, e.g. com.piratetok:live, instead of overflowing its column). */
.detail__libs a {
  display: grid;
  grid-template-columns: 1.5rem minmax(0, 1fr);
  column-gap: 0.55rem;
  align-items: center;
  color: var(--grey);
  text-decoration: none;
}

/* The library's own icon from the PirateTok site, at its natural ratio. */
.detail__libs img {
  grid-row: 1 / 3;
  width: 1.5rem;
  height: 1.5rem;
  object-fit: contain;
}

.detail__libs a span {
  color: var(--matrix);
  line-height: 1.2;
}

.detail__libs a .pkg {
  color: var(--grey);
  font-size: 0.74rem;
  overflow-wrap: anywhere;
}

.detail__libs a:hover,
.detail__libs a:focus-visible {
  color: var(--matrix);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.detail__mark {
  width: clamp(4rem, 10vh, 6.5rem);
  height: clamp(4rem, 10vh, 6.5rem);
  object-fit: contain;
}

.detail__mark--text {
  display: grid;
  place-items: center;
  border: 1px solid currentColor;
  border-radius: 50%;
  font-size: 1.5rem;
  font-weight: 700;
}

.detail__title {
  font-size: clamp(1.4rem, 2.4vw, 2.3rem);
  font-weight: 700;
  letter-spacing: 0.05em;
  line-height: 1.1;
  text-transform: uppercase;
  text-shadow: 0 0 10px var(--glow);
  overflow-wrap: anywhere;
}

.detail__title--crate {
  text-transform: none;
  letter-spacing: 0.01em;
}

.detail__title:focus {
  outline: none;
}

.detail__title:focus-visible {
  outline: 1px dashed var(--dim);
  outline-offset: 4px;
}

.detail__lede {
  grid-column: 1 / -1;
  color: #d6e4d6;
  font-size: 0.95rem;
  line-height: 1.55;
}

.detail__facts {
  grid-column: 1 / -1;
  display: grid;
  gap: 0.3rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--hair);
  font-size: 0.86rem;
}

.detail__facts div {
  display: grid;
  grid-template-columns: 8rem minmax(0, 1fr);
  gap: 1rem;
}

.detail__facts dt {
  color: var(--dim);
  font-size: 0.74rem;
  letter-spacing: 0.12em;
  line-height: 1.6;
  text-transform: uppercase;
}

/* The two exits, centred at the bottom: the project's page and its source. */
.detail__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.6rem;
  margin-top: auto;
  padding-top: 0.3rem;
}

.detail__go .i {
  flex: none;
  width: 1.1em;
  height: 1.1em;
}

.detail__go {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.65rem 1.1rem;
  border: 1px solid var(--matrix);
  background: var(--matrix);
  color: var(--bg);
  font-size: 0.86rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-decoration: none;
  text-transform: uppercase;
  transition: background-color 120ms linear;
}

.detail__go:hover,
.detail__go:focus-visible {
  background: #9dff6e;
  outline: 2px solid var(--matrix);
  outline-offset: 3px;
}

/* View on GitHub: the same bright action, outlined. */
.detail__go--alt {
  background: transparent;
  color: var(--matrix);
}

.detail__go--alt:hover,
.detail__go--alt:focus-visible {
  background: rgb(81 255 13 / 0.12);
}

@media (prefers-reduced-motion: no-preference) {
  .detail__slug::after {
    animation: blink 1.6s steps(1);
  }
}

@keyframes blink {
  50% { opacity: 0; }
}

/* ---- FIRMWARE: chip pinout, traces, readout bus, dump cells ----------------- */
.fw-trace,
.fw-bus,
.fw-pkg {
  fill: none;
  stroke: var(--deco);
  stroke-linejoin: round;
}

.fw-trace {
  stroke-width: 1.2;
  opacity: 0.6;
}

.fw-bus {
  stroke-width: 1.5;
  opacity: 0.85;
}

.fw-pkg {
  stroke-width: 2.2;
  filter: drop-shadow(0 0 2px var(--deco-glow));
}

.fw-pins,
.fw-pin1 {
  fill: var(--deco);
}

.fw-cell {
  fill: transparent;
  stroke: var(--deco-dim);
  stroke-width: 0.8;
  transition: fill 90ms linear;
}

.fw-cell.on {
  fill: var(--deco);
  stroke: var(--deco);
}

/* While the dump runs, packets travel down the bus into the cells. */
.panel--fw.is-dumping .fw-bus {
  stroke-dasharray: 3 7;
  animation: fw-packets 0.9s linear;
}

@keyframes fw-packets {
  to { stroke-dashoffset: -10; }
}

.fw-map {
  list-style: none;
  margin-top: 0.6rem;
}

.fw-map li {
  display: grid;
  grid-template-columns: 9ch 7ch 1fr;
  gap: 0.6ch;
  color: var(--grey);
  transition: opacity 200ms linear;
}

.fw-map li span:nth-child(2) {
  color: var(--matrix);
}

.fw-map li.pending {
  opacity: 0;
}

/* ---- REVERSE ENGINEERING: objdump-style listing ------------------------------
 * address | encoded bytes | mnemonic | operands. The highlight steps through
 * the rows; the match fills its row and brackets its bytes. */
.re-asm {
  list-style: none;
}

.re-asm li {
  display: grid;
  grid-template-columns: 5ch 12.5ch 5.5ch minmax(0, 1fr);
  padding: 0 0.4rem;
  margin: 0 -0.4rem;
  white-space: nowrap;
}

.re-asm li span:first-child {
  color: var(--dim);
}

.re-asm .op {
  color: var(--grey);
}

.re-asm li.cur {
  background: rgb(78 111 62 / 0.28);
}

.re-asm li.hit,
.re-asm li.hit span {
  background: var(--deco);
  color: #eef7e8;
}

.re-asm .op.hit {
  justify-self: start;
  outline: 1px solid var(--bg);
  outline-offset: -1px;
  padding-inline: 0.15em;
  margin-left: -0.15em;
}

.re-at {
  color: var(--grey);
  letter-spacing: 0.02em;
  text-transform: none;
}

/* ---- JOURNALISM: sources pinned by red strings into one story -------------- */
.j-box,
.j-frame {
  fill: rgb(78 111 62 / 0.06);
  stroke: var(--deco);
  stroke-width: 1.2;
}

.j-grid {
  fill: none;
  stroke: var(--deco-dim);
  stroke-width: 0.6;
}

.j-scan {
  fill: var(--deco);
  opacity: 0;
}

.j-chip,
.j-tick {
  fill: none;
  stroke: var(--deco);
  stroke-width: 1.3;
}

.j-text {
  fill: var(--matrix);
  font-family: var(--font-term);
  font-size: 10px;
}

.j-string {
  fill: none;
  stroke: var(--redstring);
  stroke-width: 1.6;
  stroke-dasharray: 100;
}

.j-pin {
  fill: var(--deco);
}

.j-bar,
.j-line {
  fill: var(--deco);
  transform-box: fill-box;
  transform-origin: left center;
}

.j-line {
  opacity: 0.55;
}

/* ---- ACTIVISM: from Timișoara out to every origin ---------------------------
 * The card's background: land as a dim dot matrix (pattern fill of the
 * Natural Earth outlines), the arcs and nodes the only bright marks. The
 * map spans the card's width and sits behind its open band and the press
 * list, whose backing keeps the text legible. */
.panel__bg .act-map {
  width: 100%;
  height: auto;
  overflow: hidden;
}

#act-dots circle {
  fill: rgb(78 111 62 / 0.42);
}


.a-land {
  fill: url(#act-dots);
}

.a-arc {
  fill: none;
  stroke: var(--deco);
  stroke-width: 1.1;
  stroke-linecap: round;
  stroke-dasharray: 100;
  filter: drop-shadow(0 0 1.5px var(--deco-glow));
}

.a-node circle:first-child,
.a-anchor {
  fill: var(--bg);
  stroke: var(--deco);
  stroke-width: 1;
}

.a-ring {
  fill: none;
  stroke: var(--deco);
  stroke-width: 0.8;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
}

.a-dot {
  fill: var(--deco);
}

.a-node {
  transform-box: fill-box;
  transform-origin: center;
}

/* ---- motion -----------------------------------------------------------------
 * Frames open like the film's CRT panels and traces draw in on load. JOUR
 * and ACT replay when panels.js adds .play; .armed holds their first frame
 * while they wait their turn. Without motion everything shows its result. */
@media (prefers-reduced-motion: no-preference) {
  .frame {
    animation: frame-open 420ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
  }

  /* The project window is a CRT switch, one keyframe set both ways and no
   * fade. Closing (.closing, projects.js hides when it ends): the frame
   * clips top and bottom to a 2px green line across its middle, the line
   * shrinks from both ends to a point, then it is gone. Opening (.opening)
   * runs the same keyframes in reverse: point, line, frame. The card
   * frame-open does not apply to it. */
  .detail {
    animation: none;
  }

  .detail.closing {
    animation: frame-close 820ms linear both;
  }

  .detail.opening {
    animation: frame-close 820ms linear both reverse;
  }

  .detail.closing::after,
  .detail.opening::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 2px;
    margin-top: -1px;
    background: var(--matrix);
    pointer-events: none;
    z-index: 4;
  }

  /* The line shows only while the frame is at it: from the end of the
   * closing clip, and until the end of the opening line phase. */
  .detail.closing::after {
    animation: line-show 500ms steps(1, end) both;
  }

  .detail.opening::after {
    animation: line-hide 320ms steps(1, end) both;
  }

  .panel--fw { animation-delay: 100ms; }
  .panel--re { animation-delay: 200ms; }
  .panel--jour { animation-delay: 300ms; }
  .panel--act { animation-delay: 400ms; }
  .slot .frame { animation-delay: 500ms; }

  .fw-trace {
    stroke-dasharray: 100;
    animation: draw 1.4s ease-out 450ms both;
  }

  .re-asm li.hit {
    animation: re-flash 900ms ease-out;
  }

  .panel--jour.armed .j-string { stroke-dashoffset: 100; }
  .panel--jour.armed .j-tick,
  .panel--jour.armed .j-line { opacity: 0; }
  .panel--jour.armed .j-bar { transform: scaleX(0); }

  .panel--jour.play .j-scan { animation: j-scan 1.1s ease-in-out both; }
  .panel--jour.play .j-string { animation: draw 450ms ease-out 0.9s both; }
  .panel--jour.play .j-string--late { animation-delay: 1.4s; }
  .panel--jour.play .j-tick { animation: appear 200ms linear 1.9s both; }
  .panel--jour.play .j-bar { animation: fill-x 700ms ease-out 2.1s both; }
  .panel--jour.play .j-line { animation: appear-dim 300ms linear 2.8s both; }

  .panel--act.armed .a-arc { stroke-dashoffset: 100; }
  .panel--act.armed .a-node { transform: scale(0); }

  .panel--act.play .a-arc { animation: draw 700ms ease-out both; }
  .panel--act.play .a-node { animation: pop 300ms ease-out both; }
  .panel--act.play .a-ring { animation: ring 1.4s ease-out both; }
  /* Stagger: each arc leaves 150 ms after the last and its node pops as it
   * lands. path:nth-of-type(1) is the land, so arcs are paths 2..6. */
  .play .a-arc:nth-of-type(3) { animation-delay: 0.15s; }
  .play .a-arc:nth-of-type(4) { animation-delay: 0.3s; }
  .play .a-arc:nth-of-type(5) { animation-delay: 0.45s; }
  .play .a-arc:nth-of-type(6) { animation-delay: 0.6s; }
  .play .a-node:nth-of-type(1) { animation-delay: 0.6s; }
  .play .a-node:nth-of-type(2) { animation-delay: 0.75s; }
  .play .a-node:nth-of-type(3) { animation-delay: 0.9s; }
  .play .a-node:nth-of-type(4) { animation-delay: 1.05s; }
  .play .a-node:nth-of-type(5) { animation-delay: 1.2s; }
}

@keyframes frame-open {
  from { transform: scaleY(0.03); opacity: 0; }
  60% { opacity: 1; }
  to { transform: none; opacity: 1; }
}

/* A: 0-500ms the frame clips to the 2px line; B: 500-820ms the line clips
 * from both ends to a point. No opacity anywhere; the end is hidden by JS. */
@keyframes frame-close {
  0% { clip-path: inset(0 0 0 0); animation-timing-function: ease-in; }
  61% { clip-path: inset(calc(50% - 1px) 0 calc(50% - 1px) 0); animation-timing-function: ease-in; }
  100% { clip-path: inset(calc(50% - 1px) 50% calc(50% - 1px) 50%); }
}

/* The line stays hidden until phase A has closed onto it (closing) and
 * stays shown until the line has grown to full width (opening). */
@keyframes line-show {
  from { visibility: hidden; }
  to { visibility: visible; }
}

@keyframes line-hide {
  from { visibility: visible; }
  to { visibility: hidden; }
}

/* ---- replay: a click inside a card restarts its visual's animations ---------
 * panels.js adds .replay, reflows, removes it: with no animation-name the
 * running animations are dropped, and restoring the rule starts them over. */
.panel.replay .panel__viz,
.panel.replay .panel__viz *,
.panel.replay .panel__viz *::after,
.panel.replay .panel__bg,
.panel.replay .panel__bg * {
  animation-name: none !important;
}

@keyframes draw {
  from { stroke-dashoffset: 100; }
  to { stroke-dashoffset: 0; }
}

@keyframes re-flash {
  from { box-shadow: 0 0 10px 2px var(--glow); }
  to { box-shadow: none; }
}

@keyframes j-scan {
  from { transform: translateX(0); opacity: 0.9; }
  90% { opacity: 0.9; }
  to { transform: translateX(122px); opacity: 0; }
}

@keyframes appear {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes appear-dim {
  from { opacity: 0; }
  to { opacity: 0.55; }
}

@keyframes fill-x {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

@keyframes pop {
  from { transform: scale(0); }
  70% { transform: scale(1.4); }
  to { transform: scale(1); }
}

@keyframes ring {
  from { transform: scale(1); opacity: 0.9; }
  to { transform: scale(3); opacity: 0; }
}

/* ---- short desktop viewports ------------------------------------------------
 * Up to 768 px tall (a 1366x768 screen, or ~650 px of it under browser
 * bars). Two panels must still stack in that height without page scroll:
 * tighten the rhythm, never the content (marks stay larger than 1.7rem). */
/* Three cards per side share the viewport height, so shorter screens drop
 * the decorative extras first: the status readouts and slot labels below
 * 992 px, the illustrative visual bands below 704 px. Titles, captions,
 * project entries and press links always stay. */
@media (max-height: 62rem) {
  .panel__status,
  .slot__label {
    display: none;
  }

  /* Library group labels are structure, not decoration: they stay. */
  .slot--lib .slot__label {
    display: block;
  }

  .panel {
    padding: 0.45rem 0.85rem 0.45rem;
  }

  .panel__title {
    font-size: 0.95rem;
    padding-bottom: 0.2rem;
    margin-bottom: 0.4rem;
  }

  .panel__caption {
    margin: 0.2rem 0 0.35rem;
  }

  .panel__body {
    gap: 0.4rem;
    margin-bottom: 0;
  }

  .entry {
    padding: 0.3rem 0.45rem;
  }

  .entry__meta {
    line-height: 1.25;
  }

  .entry--press {
    padding: 0.3rem 0.55rem;
  }
}

@media (max-height: 44rem) {
  .panel__viz {
    display: none;
  }

  /* The project window keeps every line; only its spacing tightens. */
  .detail__bar {
    padding-block: 0.35rem;
  }

  .detail__body {
    gap: 0.5rem;
    padding: 0.8rem 1.2rem 0.9rem;
  }

  .detail__mark {
    width: 3.2rem;
    height: 3.2rem;
  }

  .detail__facts {
    padding-top: 0.45rem;
    gap: 0.15rem;
  }

  .detail__libs {
    row-gap: 0.3rem;
  }

  .detail__actions {
    padding-top: 0;
  }
}

/* ---- desktop: visuals fill each card; the project window fills the centre
 * Rows share the viewport (style.css), so after its title, caption and
 * project list each card has some height left: the visual takes all of it
 * (a size container), and text visuals scale their type to that box. The
 * project window covers the whole centre column, out of flow, so opening it
 * changes nothing around it. */
@media (min-width: 80.01rem) {
  .panel__viz {
    flex: 1 1 0;
    height: auto;
    min-height: 3.6rem;
    container-type: size;
  }

  /* Firmware keeps to its content (title, visual, contact): its visual has
   * a set height instead of growing. */
  .panel--fw .panel__viz {
    flex: 0 0 auto;
    height: clamp(4.5rem, 12vh, 8.5rem);
  }

  .panel--fw .fw-map {
    font-size: clamp(0.72rem, min(15cqh, 3.1cqw), 1.05rem);
  }

  .panel--re .re-asm {
    font-size: clamp(0.72rem, min(16cqh, 4.4cqw), 1.2rem);
  }

  .aidx-log {
    font-size: clamp(0.72rem, min(16cqh, 5.2cqw), 1.2rem);
  }

  /* Reverse engineering and AiDX keep to their content like Firmware (the
   * AiDX log gets a set, shorter height), so the column's spare height goes
   * to the Libraries list. */
  .side > .panel--re,
  .side > .panel--aidx {
    flex: 0 0 auto;
  }

  .panel--aidx .panel__viz {
    flex: 0 0 auto;
    height: clamp(3.6rem, 6vh, 4.5rem);
  }

  /* The library list takes the card's share of the column rather than its
   * own content height (which would push the hero past one screen); what
   * does not fit scrolls inside it. */
  .slot--lib {
    flex: 1 1 0;
    contain: size;
  }

  /* The window itself never scrolls: its corner brackets sit 1px outside
   * its box (.frame::before), which in a scroll container always counts as
   * overflow. Only the article under the title bar scrolls, and only when
   * its content is taller than the space left. */
  .detail {
    position: absolute;
    inset: 0;
    z-index: 3;
    width: auto;
  }

  .detail__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
  }

  /* The animation takes the height between the text and the buttons; if
   * that is too little to read, it steps aside instead of overflowing. */
  .detail__anim {
    flex: 1 1 0;
    height: auto;
    min-height: 0;
    container-type: size;
  }

  @container (height < 2.5rem) {
    .detail__anim svg {
      display: none;
    }
  }
}

/* ---- project scenes (centre window) -----------------------------------------
 * One short story per project, drawn on a 640x320 canvas (scenes.py in the
 * draft notes). The markup is the finished frame, and that is all that shows
 * without motion. With motion, projects.js puts .play on the open article's
 * .detail__anim: every a-* step waits for its --t, runs once and holds; the
 * scene (--loop) plays once per opening and then stays on its last frame.
 * s-* classes paint; a-* classes move. Shared defs (grid, glow) live in the
 * icon sprite. Red belongs to Red String and to refusals; teal and pink are
 * PirateTok's events. */
.detail__anim {
  --deco-hi: #7fa86a;
  --ink: #dcebd2;
  --teal: #4ecdc4;
  --pink: #ff6b9a;
  --scene-bg: #040d04;
}

.detail__anim svg {
  font-size: 13px;
}

/* paint: shapes */
.s-grid { fill: url(#fx-grid); mask: url(#fx-fade); }
.s-box { fill: rgb(78 111 62 / 0.08); stroke: var(--deco); stroke-width: 1.5; }
.s-boxr { fill: rgb(255 53 69 / 0.08); stroke: var(--redstring); stroke-width: 1.5; }
.s-card { fill: var(--scene-bg); stroke: var(--deco); stroke-width: 1.5; }
.s-strong { stroke: var(--deco-hi); }
.s-photo { fill: rgb(127 168 106 / 0.05); stroke: rgb(78 111 62 / 0.6); stroke-width: 1; }
.s-hot { fill: rgb(127 168 106 / 0.12); stroke: var(--deco-hi); stroke-width: 2; filter: url(#fx-glow); }
.s-hotr { fill: rgb(255 53 69 / 0.08); stroke: var(--redstring); stroke-width: 2; filter: url(#fx-glow); }
.s-tag { fill: rgb(20 4 6); stroke: rgb(255 53 69 / 0.7); stroke-width: 1; }
.s-veil { fill: rgb(2 8 2 / 0.72); }
.s-sky { fill: rgb(78 111 62 / 0.14); stroke: var(--deco); stroke-width: 1.2; }
.s-cell { fill: var(--scene-bg); stroke: rgb(78 111 62 / 0.4); stroke-width: 0.8; }
.s-screen { fill: rgb(14 6 16); stroke: rgb(78 111 62 / 0.5); stroke-width: 1; }
.s-scan { fill: rgb(127 168 106 / 0.28); }
.s-coin { fill: #c9d3c3; stroke: var(--deco); stroke-width: 1.2; }
.s-docline { stroke: var(--deco-hi); }
.s-pin { fill: var(--redstring); }
.s-pinhi { fill: #ffc2c7; }

.s-line, .s-dim, .s-faint, .s-red, .s-ink, .s-pkt, .s-pinkline {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.s-line { stroke: var(--deco-hi); stroke-width: 2; }
.s-dim { stroke: var(--deco); stroke-width: 1.5; }
.s-faint { stroke: var(--deco); stroke-width: 1; stroke-dasharray: 2 5; opacity: 0.6; }
.s-red { stroke: var(--redstring); stroke-width: 1.8; }
.s-ink { stroke: var(--ink); stroke-width: 3; }
.s-pinkline { stroke: var(--pink); stroke-width: 2; }
.s-pkt { stroke-width: 4; stroke-dasharray: 0.03 0.22; }
.s-pkt-t { stroke: var(--teal); }
.s-pkt-p { stroke: var(--pink); stroke-dashoffset: var(--o, 0); }

.s-f { fill: var(--deco); }
.s-fh { fill: var(--deco-hi); }
.s-fdim { fill: rgb(78 111 62 / 0.45); }
.s-fi { fill: var(--ink); }
.s-fr { fill: var(--redstring); }
.s-ft { fill: var(--teal); }
.s-fp { fill: var(--pink); }
.s-fbg { fill: var(--scene-bg); }
.s-glow { filter: url(#fx-glow); }
.s-px { image-rendering: pixelated; }

/* paint: text */
.s-t { fill: var(--deco-hi); }
.s-td { fill: var(--deco); }
.s-ti { fill: var(--ink); }
.s-tr { fill: var(--redstring); }
.s-tt { fill: var(--teal); }
.s-tp { fill: var(--pink); }
.s-tbg { fill: #000; }
.s-kw { fill: var(--deco-hi); font-weight: 700; }
.s-xs { font-size: 10px; }
.s-sm { font-size: 11px; }
.s-md { font-size: 12px; }
.s-lg { font-size: 18px; }
.s-b { font-weight: 700; }
.s-cap { letter-spacing: 0.12em; }
.s-mid { text-anchor: middle; }
.s-end { text-anchor: end; }

/* motion: one-shot steps hold their finished state; transient ones
 * (packets, scans, flashes, refusals) are hidden in the finished frame */
.a-pop, .a-stamp, .a-turn, .a-float { transform-box: fill-box; transform-origin: center; }
.a-type, .a-grow { transform-box: fill-box; transform-origin: left center; }
.a-wipe { transform-box: fill-box; transform-origin: center top; }
.a-out, .a-blip, .a-fly, .a-flash, .a-sweep, .a-pulse { opacity: 0; }
.a-draw { stroke-dasharray: 1; }
.a-pulse { stroke-dasharray: 0.09 2; }

@media (prefers-reduced-motion: no-preference) {
  .detail__anim.play svg { animation: scene-cycle var(--loop) linear both; }
  .play .a-in { animation: a-in 0.5s var(--t, 0s) both; }
  .play .a-pop { animation: a-pop 0.45s var(--t, 0s) cubic-bezier(0.2, 1.5, 0.4, 1) both; }
  .play .a-up { animation: a-up 0.5s var(--t, 0s) ease-out both; }
  .play .a-stamp { animation: a-stamp 0.4s var(--t, 0s) cubic-bezier(0.3, 1.5, 0.5, 1) both; }
  .play .a-turn { animation: a-turn 0.7s var(--t, 0s) ease-in-out both; }
  .play .a-draw { animation: a-draw var(--dur, 0.6s) var(--t, 0s) ease-in-out both; }
  .play .a-type { animation: a-type var(--dur) var(--t, 0s) steps(var(--n)) both; }
  .play .a-grow { animation: a-grow var(--dur) var(--t, 0s) linear both; }
  .play .a-wipe { animation: a-grow-y var(--dur) var(--t, 0s) linear both; }
  .play .a-out { animation: a-out 0.3s var(--t, 0s) both; }
  .play .a-blip { animation: a-blip var(--hold) var(--t, 0s) both; }
  .play .a-fly { animation: a-fly var(--dur) var(--t, 0s) var(--ease, cubic-bezier(0.5, 0, 0.3, 1)) both; }
  .play .a-flash { animation: a-flash 0.7s var(--t, 0s) ease-out both; }
  .play .a-sweep { animation: a-sweep var(--dur) var(--t, 0s) ease-in-out both; }
  .play .a-pulse { animation: a-pulse var(--dur) var(--t, 0s) ease-in-out both; }
  /* ambient: one pass each, then still */
  .play .a-blink { animation: a-blink 1s var(--t, 0s) steps(1); }
  .play .a-flow { animation: a-flow 0.6s linear; }
  .play .a-float { animation: a-float var(--dur) var(--t, 0s) ease-out both; }
  .play .a-scroll { animation: a-scroll var(--dur) linear; }
}

/* The scene fades in once and holds its finished frame. */
@keyframes scene-cycle {
  0% { opacity: 0; }
  2.5%, 100% { opacity: 1; }
}

@keyframes a-in { from { opacity: 0; } }
@keyframes a-pop { from { opacity: 0; transform: scale(0.3); } }
@keyframes a-up { from { opacity: 0; transform: translateY(var(--dy, 14px)); } }
@keyframes a-stamp { from { opacity: 0; transform: scale(1.7); } }
@keyframes a-turn { from { transform: rotate(-180deg); } }
@keyframes a-draw { from { stroke-dashoffset: 1; } }
@keyframes a-type { from { transform: scaleX(0); } }
@keyframes a-grow { from { transform: scaleX(0); } }
@keyframes a-grow-y { from { transform: scaleY(0); } }
@keyframes a-out { from { opacity: 1; } }

@keyframes a-blip {
  0% { opacity: 0; }
  5%, 94% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes a-fly {
  0% { opacity: 0; transform: none; }
  12%, 88% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--dx, 0px), var(--dy, 0px)); }
}

@keyframes a-flash {
  0% { opacity: 0; }
  15% { opacity: var(--peak, 0.8); }
  100% { opacity: 0; }
}

@keyframes a-sweep {
  0% { opacity: 0; transform: none; }
  10%, 85% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--dx, 0px), var(--dy, 0px)); }
}

@keyframes a-pulse {
  0% { opacity: 1; stroke-dashoffset: 0.09; }
  90% { opacity: 1; }
  100% { opacity: 0; stroke-dashoffset: -1; }
}

@keyframes a-blink { 50% { opacity: 0; } }

@keyframes a-flow {
  from { stroke-dashoffset: var(--o, 0); }
  to { stroke-dashoffset: calc(var(--o, 0) - 0.25); }
}

@keyframes a-float {
  0% { opacity: 0; transform: translate(0, 0) scale(0.6); }
  15% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--dx, 0px), -150px) scale(1.1); }
}

@keyframes a-scroll { to { transform: translateX(var(--dx)); } }

/* ---- AiDX & Tooling: an MCP caller query, then a rule check ---------------- */
.aidx-log {
  list-style: none;
  display: grid;
  align-content: center;
  height: 100%;
  font-size: 0.72rem;
  line-height: 1.25;
  white-space: nowrap;
  overflow-wrap: normal;
}

.aidx-log li:not(:has(.aidx-p)) {
  padding-left: 2ch;
}

.aidx-p,
.aidx-ok {
  color: var(--matrix);
}

.aidx-arg {
  color: #d6e4d6;
}

.aidx-dim {
  color: var(--dim);
  margin-right: 1ch;
}

.aidx-cursor::after {
  content: "_";
  margin-left: 0.3ch;
}

/* ---- Libraries: crates grouped by domain --------------------------------------
 * No visual band: the grouped list is the whole body. The list scrolls inside
 * the card when the column is too short for it; a shadow at its bottom edge
 * shows only while rows are hidden below (background-attachment: local). */
.side > .panel--lib {
  flex: 2 1 auto;
}

.slot--lib {
  min-height: 0;
  overflow-y: auto;
  padding: 2px;
  scrollbar-width: thin;
  scrollbar-color: var(--hair) transparent;
  background:
    linear-gradient(var(--panel-bg), var(--panel-bg)) bottom / 100% 1.2rem no-repeat,
    linear-gradient(to top, rgb(81 255 13 / 0.2), transparent) bottom / 100% 1.2rem no-repeat;
  background-attachment: local, scroll;
}

/* Groups in two equal columns; a group's tiles stack one per row at the
 * group's width (two-up when it holds several), so every tile is the same
 * size across groups. */
.slot--lib {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-content: start;
  column-gap: 0.5rem;
  row-gap: 0.35rem;
}

/* The group with several crates takes the full card width (its tiles two-up
 * are then half the card, like every other tile); the single-crate groups
 * fill the two columns under it. A hair rule separates the rows. */
.lib-group:has(> .slot__list > li + li) {
  grid-column: 1 / -1;
}

.lib-group + .lib-group {
  padding-top: 0.3rem;
  border-top: 1px solid var(--hair);
}

/* Phones (style.css, one column): one group per row. */
@media (max-width: 40rem) {
  .slot--lib {
    grid-template-columns: minmax(0, 1fr);
  }
}

.slot--lib .slot__list {
  grid-template-columns: minmax(0, 1fr);
  gap: 0.3rem;
}

/* A group with several crates lays them two-up (2x2 for four). */
.slot--lib .slot__list:has(> li + li) {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.entry--lib {
  --arm: 0.45rem;
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  padding: 0.3rem 0.5rem;
  text-align: center;
}

/* Crate names stay on one line: clipped with an ellipsis, never broken. */
.entry--lib .entry__name {
  font-size: 0.86rem;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- Decentralized infrastructure: confirmed blocks carrying records ------- */
.c-link {
  fill: none;
  stroke: var(--deco-dim);
  stroke-width: 1.4;
}

.c-block rect:first-child {
  fill: rgb(78 111 62 / 0.08);
  stroke: var(--deco);
  stroke-width: 1.4;
}

.c-rec {
  fill: var(--deco);
  opacity: 0.75;
}

.c-block--new rect:first-child {
  stroke-dasharray: 3 3;
}

@media (prefers-reduced-motion: no-preference) {
  .aidx-cursor::after {
    animation: blink 1.6s steps(1);
  }

  .c-block--new {
    animation: confirm 4.8s ease-in-out;
  }
}

@keyframes confirm {
  0%, 40% { opacity: 0.35; }
  60%, 100% { opacity: 1; }
}
