/* DeckLogic product site — the brand system (BRAND-5).
 *
 * Canonical spec: docs/product/design/2026-08-12-brand-marks-wordmarks.html
 * (W1 Signal Split, W2 Dead Tube, W3 Attach Sequence, W4 Decrypt, W5 Trace
 * Route, M1 Slipstack). Every treatment below is DERIVED from what the app
 * already ships rather than re-drawn from the prototype:
 *
 *   Signal Split  <- src/lib/motion/plot.css   (BRAND-3 keyframes, verbatim)
 *   Decrypt       <- src/lib/motion/plot.css   (BRAND-4 letter states)
 *   Trace Route   <- src/lib/components/BootSurface.svelte (BRAND-2)
 *   Attach Seq.   <- src/lib/components/BootSurface.svelte (BRAND-2 .terminal)
 *   Module chrome <- site/assets/css/tokens.css  (--module-clip, brackets)
 *
 * This is the ONE shared site asset for all of it: no page carries a divergent
 * copy. The single exception is site/404.html, which is served at arbitrary
 * missing paths and therefore may not link a stylesheet at all — its Dead Tube
 * treatment is inlined there by necessity and appears nowhere else.
 *
 * MOTION AUTHORITY. The app's rule is motion-as-event, no idle motion, with
 * exactly one governed exception (BRAND-3's pulse). Marketing pages tolerate
 * more motion, but the two hard lines still hold here: reduced-motion users get
 * still pages, and nothing loops without a rest state. Two effects on this site
 * recur — the Signal Split pulse and the Trace Route replay — and they share a
 * single cadence, which lives in site/assets/js/brand.js and nowhere else.
 * Everything else (Decrypt, Slipstack) is event-shaped and amends nothing.
 *
 * Reduced motion is read in exactly two places on this site: the one media
 * query at the foot of this file, and prefersReducedMotion() in brand.js —
 * mirroring src/lib/motion/reducedMotion.ts being the app's only reader.
 */

/* ============================================================
   W1 · SIGNAL SPLIT — the brand pulse
   ============================================================
   Ghost layers are real elements built by brand.js, not ::before/::after
   reading attr(data-text) as the app does. The site's headings carry markup
   (<br /> line breaks on four of the six pages) and attr() can only reproduce
   a flat string, so a pseudo-element ghost would print a two-line title as one
   long line. Cloned spans reproduce the markup exactly and stay out of the
   accessibility tree. The keyframes below are BRAND-3's, unchanged. */
[data-brand-pulse] {
  position: relative;
  isolation: isolate;
}
.bp-ink {
  position: relative;
  z-index: 1;
}
.bp-ghost {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  user-select: none;
}
.bp-pink {
  color: var(--accent-2);
  z-index: -1;
}
.bp-yellow {
  color: var(--accent);
  z-index: -2;
}
.brand-pulse-fire {
  animation: brand-pulse-base var(--motion-brand-pulse) 1 both;
}
.brand-pulse-fire .bp-pink {
  animation: brand-pulse-pink var(--motion-brand-pulse) 1 both;
}
.brand-pulse-fire .bp-yellow {
  animation: brand-pulse-yellow var(--motion-brand-pulse) 1 both;
}
@keyframes brand-pulse-base {
  0%,
  100% { transform: none; }
  14.29% { transform: skewX(-4deg); }
  28.57% { transform: skewX(3deg) translateX(2px); }
  50% { transform: none; }
  64.29% { transform: translateX(-2px); }
  78.57% { transform: none; }
}
@keyframes brand-pulse-pink {
  0%,
  100% { opacity: 0; transform: none; clip-path: inset(0 0 0 0); }
  7.14% { opacity: 0.9; transform: translate(-6px, 2px); clip-path: inset(12% 0 55% 0); }
  28.57% { opacity: 0.9; transform: translate(5px, -1px); clip-path: inset(58% 0 8% 0); }
  50% { opacity: 0.9; transform: translate(-4px, 0); clip-path: inset(30% 0 38% 0); }
  71.43% { opacity: 0.7; transform: translate(3px, 1px); clip-path: inset(70% 0 4% 0); }
  85.71% { opacity: 0; }
}
@keyframes brand-pulse-yellow {
  0%,
  100% { opacity: 0; transform: none; clip-path: inset(0 0 0 0); }
  7.14% { opacity: 0.9; transform: translate(6px, -2px); clip-path: inset(55% 0 14% 0); }
  28.57% { opacity: 0.9; transform: translate(-5px, 1px); clip-path: inset(6% 0 62% 0); }
  50% { opacity: 0.9; transform: translate(4px, 0); clip-path: inset(44% 0 22% 0); }
  71.43% { opacity: 0.7; transform: translate(-3px, -1px); clip-path: inset(2% 0 78% 0); }
  85.71% { opacity: 0; }
}

/* ============================================================
   W4 · DECRYPT — headings decode as they enter the viewport
   ============================================================
   Plain colour swaps, not keyframes: brand.js drives the per-letter schedule
   directly and a second timeline here would argue with it. A letter carries no
   class while it is still cipher, so the unstyled state IS the scrambled one.
   min-width stops the heading reflowing as glyphs of different widths cycle. */
[data-decrypting] > span {
  display: inline-block;
  min-width: 0.62em;
  text-align: center;
  color: var(--data-magenta);
  opacity: 0.65;
}
[data-decrypting] > span.decrypt-locked {
  color: var(--text);
  opacity: 1;
}
[data-decrypting] > span.decrypt-flash {
  color: var(--accent);
  opacity: 1;
}

/* ============================================================
   W5 · TRACE ROUTE — the index hero wordmark
   ============================================================
   Structure and keyframes are BRAND-2's boot surface: an etched stroke layer,
   a fill layer that pours in behind it, and a pink scan head. At rest (no
   .is-drawing class) the final frame is what renders, which is exactly what a
   visitor with JS disabled or animations unavailable sees. The viewBox gives
   the element an intrinsic aspect ratio, so the space is reserved before the
   SVG paints and the draw causes no layout shift. */
.brand-hero {
  /* How long one draw takes. A style constant, not a cadence: WHEN the draw
     repeats is brand.js's nextGapMs() and lives only there. The prototype's
     7.5s loop is the draw plus dead air; this is the draw. */
  --brand-trace-dur: 2600ms;
  position: relative;
  width: min(620px, 100%);
  margin: 0 0 22px;
}
.brand-hero svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}
.brand-hero text {
  font-family: var(--font);
  font-size: 78px;
  font-weight: 800;
  letter-spacing: 6px;
}
.brand-hero .trace-etch {
  fill: transparent;
  stroke: var(--accent);
  stroke-width: 1.5;
  opacity: 0.28;
  stroke-dasharray: 26 8;
}
.brand-hero .trace-fill {
  fill: var(--text);
  stroke: var(--accent);
  stroke-width: 1;
  paint-order: stroke;
  filter: drop-shadow(0 0 14px rgb(from var(--accent) r g b / 0.18));
}
.brand-hero .scan-head {
  fill: var(--accent-2);
  opacity: 0;
  filter: drop-shadow(0 0 8px var(--accent-2));
}
.brand-hero.is-drawing .trace-etch { animation: trace-draw var(--brand-trace-dur) both; }
.brand-hero.is-drawing .trace-fill { animation: trace-fill var(--brand-trace-dur) both; }
.brand-hero.is-drawing .scan-head { animation: trace-head var(--brand-trace-dur) both; }
@keyframes trace-draw {
  0% { stroke-dasharray: 0 34; opacity: 0.12; }
  70% { stroke-dasharray: 26 8; opacity: 1; }
  100% { opacity: 0.28; }
}
@keyframes trace-fill {
  0%,
  48% { fill: transparent; opacity: 0.2; }
  100% { fill: var(--text); opacity: 1; }
}
@keyframes trace-head {
  0% { transform: translateX(10px); opacity: 0; }
  12%,
  72% { opacity: 1; }
  82%,
  100% { transform: translateX(600px); opacity: 0; }
}

/* ============================================================
   W3 · ATTACH SEQUENCE — the product preview terminal
   ============================================================
   The same block BRAND-2 shows while the app starts: the command types itself,
   then the status lines answer. Static markup, so a visitor with JS off reads
   the finished sequence. */
.attach-term {
  position: relative;
  max-width: 34rem;
  padding: var(--s-6);
  background: rgb(from var(--surface-1) r g b / 0.94);
  border: 1px solid rgb(from var(--accent) r g b / 0.38);
  clip-path: var(--module-clip);
  font-family: var(--mono);
  font-size: 0.9rem;
  line-height: 1.9;
}
.attach-term::before,
.attach-term::after {
  content: "";
  position: absolute;
  width: 18px;
  height: 8px;
  border-color: var(--accent);
  opacity: 0.8;
}
.attach-term::before { top: 7px; left: 7px; border-top: 1px solid; border-left: 1px solid; }
.attach-term::after { right: 7px; bottom: 7px; border-right: 1px solid; border-bottom: 1px solid; }
.attach-term p {
  margin: 0;
}
.attach-term .command {
  color: var(--text);
  font-weight: 700;
  margin-bottom: var(--s-3);
  white-space: nowrap;
}
.attach-term .prompt { color: var(--accent); }
.attach-term .caret {
  display: inline-block;
  width: 7px;
  height: 1em;
  margin-left: 3px;
  vertical-align: -2px;
  background: var(--accent);
}
.attach-term .stage-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
  font-variant-numeric: tabular-nums;
}
.attach-term .flag { color: var(--status-good); font-weight: 800; }
.attach-term .label { color: var(--text); font-weight: 700; }
.attach-term .detail { color: var(--text-muted); }
.attach-term .detail.we { color: var(--data-magenta); }
/* The LIVE chip sits on opaque --accent with --accent-contrast ink: the app's
   emphasis rule, because a saturated mid-luminance hue cannot be guaranteed
   against an arbitrary backdrop at partial opacity. */
.attach-term .live {
  padding: 2px 6px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-weight: 900;
  line-height: 1.4;
}
/* Typed reveal. "> decklogic --attach" is 20 characters, so the reveal runs in
   20 steps; the settled width is deliberately wider than the text, because the
   animation holds its end state and a width equal to the content would clip
   the line permanently in any font whose "0" is narrower than its letters.
   The answer lines hold until the command finishes. */
.attach-term.is-typing .command {
  width: 22ch;
  overflow: hidden;
  animation: attach-type var(--m-impact) steps(20, end) both;
}
.attach-term.is-typing .caret { animation: attach-caret var(--m-impact) steps(1, end) 3; }
.attach-term.is-typing .stage-line { animation: attach-answer var(--m-impact) ease-out both; }
@keyframes attach-type { from { width: 0; } to { width: 22ch; } }
@keyframes attach-caret { 50% { opacity: 0; } }
@keyframes attach-answer { 0%, 60% { opacity: 0; } 100% { opacity: 1; } }

/* ============================================================
   M1 · SLIPSTACK — the mark's hover event
   ============================================================
   The mark ships as site/assets/img/logo.svg and is referenced through <img>,
   so page CSS cannot reach anything inside it. The generator (BRAND-1, now
   drawing the Stamped card, BRAND-6) owns those files and this item must not
   regenerate them, so the event is wired at the element the page actually
   controls: the same steps(2) jump, applied to the mark as a whole. */
.brand img,
.brandline img {
  transition: transform var(--m-fast) steps(2, end);
}
.brand:hover img,
.brand:focus-visible img,
.brandline:hover img {
  animation: slipstack-jump var(--m-base) var(--ease-plot);
}
@keyframes slipstack-jump {
  0% { transform: translateX(3px); }
  49% { transform: translateX(-2px); }
  100% { transform: translateX(3px); }
}

/* ============================================================
   MODULE CHROME — notched corners, terminal brackets, scanline
   ============================================================
   Applied through this shared file so no page carries its own copy. The notch
   geometry and the scanline wash are both tokens, so both themes are served:
   --wash-02 is white in the dark theme and black in the light one. */
.card,
.shot,
.cta-band {
  border-radius: 0;
  clip-path: var(--module-clip);
  background-image: repeating-linear-gradient(
    0deg,
    var(--wash-02) 0 1px,
    transparent 1px 4px
  );
}
.card {
  position: relative;
  background-color: var(--panel);
}
.card::before,
.card::after {
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  opacity: 0.7;
  pointer-events: none;
}
.card::before { top: 8px; left: 8px; border-top: 2px solid var(--accent); border-left: 2px solid var(--accent); }
.card::after { right: 8px; bottom: 8px; border-bottom: 2px solid var(--accent-2); border-right: 2px solid var(--accent-2); opacity: 0.55; }
/* The download page's numbered steps already own .card::before for their
   counter. That rule is more specific and keeps it; this only undoes the
   bracket borders it would otherwise inherit from the rule above. */
.steps .card::before {
  border: 0;
  opacity: 1;
}

/* ============================================================
   REDUCED MOTION — the one media query on this site
   ============================================================
   Every treatment renders its FINAL frame, still. site.css already stops all
   animation and transition globally; these rules exist because two of the
   effects have a resting appearance that differs from their unanimated one:
   the pulse ghosts must stay invisible rather than printing the title twice,
   and the terminal command must show its full width rather than 0. */
@media (prefers-reduced-motion: reduce) {
  .bp-ghost {
    opacity: 0 !important;
    clip-path: none !important;
    transform: none !important;
  }
  .attach-term.is-typing .command {
    width: auto;
    overflow: visible;
  }
  .attach-term.is-typing .stage-line {
    opacity: 1;
  }
}
