/* ============================================================
   Blackwall — reset, typography, shared primitives
   ============================================================ */

*, *::before, *::after { box-sizing: border-box; }

/* The horizontal guard lives on html, not body: any overflow value on body
   turns it into a scroll container, and position:sticky then resolves against
   the body box instead of the viewport - which silently breaks the nav. */
html {
  -webkit-text-size-adjust: 100%;
  overflow-x: clip;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

img, svg { max-width: 100%; }
a { color: inherit; }

/* the shared <defs> block that every mark <use> points at */
.mark-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.shell {
  width: min(1240px, 100%);
  margin-inline: auto;
  padding-inline: var(--edge);
}

/* ---------- type ---------- */

h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 700;
  line-height: 1.04;
  letter-spacing: -0.015em;
  margin: 0;
}

/* The technical eyebrow. The trailing underscore is drawn in CSS so it
   never lands in the document's text - the copy stays exactly as written. */
.label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--display);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--wall);
  margin: 0;
}
.label::before {
  content: '';
  width: 18px; height: 1px;
  background: var(--wall);
}
.label::after { content: '_'; opacity: 0.8; }

/* ---------- chamfered surfaces ---------- */

/* clip-path destroys borders, so the rule is drawn as a filled parent
   with the surface inset inside it. Both get the same cut. */
.panel {
  --c: var(--chamfer);
  position: relative;
  background: var(--hairline);
  padding: 1px;
  clip-path: var(--cut);
}
.panel--live { background: var(--wall-45); }

.panel__in {
  /* Not chamfer - 1. The inner diagonal sits at x+y = c+2b in the outer's
     coordinates, so for a true 1px perpendicular rule on the 45 degree cut
     the inner chamfer is C - b(2 - sqrt2) = C - 0.586. Using C - 1 leaves
     only 0.7px there and the rule reads thin. */
  --c: calc(var(--chamfer) - 0.586px);
  position: relative;
  height: 100%;
  background: var(--panel);
  clip-path: var(--cut);
}

/* corner brackets, drawn just inside the cut */
.brackets { position: absolute; inset: 0; pointer-events: none; }
.brackets::before,
.brackets::after {
  content: '';
  position: absolute;
  width: 16px; height: 16px;
  border: 2px solid var(--wall);
}
.brackets::before { top: 8px; right: 8px; border-left: 0; border-bottom: 0; }
.brackets::after  { bottom: 8px; left: 8px; border-right: 0; border-top: 0; }

/* ---------- buttons ---------- */

.btn {
  --c: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 13px 26px;
  font-family: var(--display);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ground);
  background: var(--wall);
  border: 0;
  clip-path: var(--cut);
  cursor: pointer;
  transition: background 180ms var(--ease-out), color 180ms var(--ease-out);
}
.btn:hover { background: var(--wall-hot); }

.btn--ghost {
  --c: 10px;
  --edge-col: var(--wall-45);
  --edge-fill: var(--panel);
  color: var(--ink);
}
/* Hover fills solid and flips the text dark. The previous version set
   the fill to a 12% red over an element whose own background is the
   solid rule colour - so the fill read as solid red, and the red text
   on top of it disappeared completely. */
.btn--ghost:hover {
  --edge-col: var(--wall-hot);
  --edge-fill: var(--wall);
  color: var(--ground);
}

/* An inset box-shadow traces the rectangular border box, so on a chamfered
   element the rule simply is not drawn along the two diagonal cuts - the
   outline looks snipped off at exactly those corners. .edge paints the rule
   as the element's own background and insets the real surface on ::before,
   both clipped, so the diagonals get a rule like every other edge. */
.edge {
  --c: var(--chamfer);
  --edge-col: var(--hairline);
  --edge-fill: var(--panel);
  position: relative;
  isolation: isolate;
  background: var(--edge-col);
  clip-path: var(--cut);
}
.edge::before {
  content: '';
  position: absolute;
  inset: 1px;
  z-index: -1;
  background: var(--edge-fill);
  clip-path: polygon(
    var(--ci) 0,
    100% 0,
    100% calc(100% - var(--ci)),
    calc(100% - var(--ci)) 100%,
    0 100%,
    0 var(--ci)
  );
  --ci: calc(var(--c) - 0.586px);
}

/* ---------- glitch ---------- */

/* Red and black only. A cyan channel reads as generic chromatic
   aberration; the Blackwall is red ICE tearing through black, so the
   effect is a red ghost plus black bars that cut the text apart. */
.glitch { position: relative; display: inline-block; }

.glitch::before,
.glitch::after {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  opacity: 0;
  pointer-events: none;
}

/* the ghost: a displaced red copy */
.glitch::before {
  content: attr(data-text);
  color: var(--wall);
  text-shadow: 0 0 12px rgba(255, 31, 61, 0.55);
}

/* the tear: black bands that slice the glyphs */
.glitch::after {
  content: attr(data-text);
  color: var(--ground);
  -webkit-text-stroke: 1px var(--wall-45);
}

.glitch.is-glitching::before,
.glitch:hover::before { animation: tearRed 540ms steps(2, end) 3; }

.glitch.is-glitching::after,
.glitch:hover::after { animation: tearDark 540ms steps(2, end) 3; }

@keyframes tearRed {
  0%   { opacity: 0;    transform: translate3d(0, 0, 0);       clip-path: inset(0 0 100% 0); }
  12%  { opacity: 0.95; transform: translate3d(-7px, -2px, 0); clip-path: inset(4% 0 74% 0); }
  28%  { opacity: 0.9;  transform: translate3d(6px, 1px, 0);   clip-path: inset(38% 0 42% 0); }
  44%  { opacity: 0.95; transform: translate3d(-5px, 2px, 0);  clip-path: inset(66% 0 14% 0); }
  60%  { opacity: 0.85; transform: translate3d(8px, -1px, 0);  clip-path: inset(18% 0 60% 0); }
  76%  { opacity: 0.9;  transform: translate3d(-3px, 0, 0);    clip-path: inset(52% 0 28% 0); }
  100% { opacity: 0;    transform: translate3d(0, 0, 0);       clip-path: inset(0 0 100% 0); }
}

@keyframes tearDark {
  0%   { opacity: 0; transform: translate3d(0, 0, 0);      clip-path: inset(100% 0 0 0); }
  16%  { opacity: 1; transform: translate3d(5px, 0, 0);    clip-path: inset(22% 0 66% 0); }
  34%  { opacity: 1; transform: translate3d(-8px, 0, 0);   clip-path: inset(58% 0 26% 0); }
  52%  { opacity: 1; transform: translate3d(4px, 0, 0);    clip-path: inset(8% 0 82% 0); }
  70%  { opacity: 1; transform: translate3d(-6px, 0, 0);   clip-path: inset(74% 0 8% 0); }
  88%  { opacity: 1; transform: translate3d(3px, 0, 0);    clip-path: inset(40% 0 48% 0); }
  100% { opacity: 0; transform: translate3d(0, 0, 0);      clip-path: inset(100% 0 0 0); }
}

/* jittery scan bars that can be laid over any element */
.tearlines {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background: repeating-linear-gradient(
    180deg,
    transparent 0 3px,
    rgba(0, 0, 0, 0.85) 3px 4px,
    transparent 4px 9px);
  mix-blend-mode: multiply;
}
.is-tearing > .tearlines { animation: tearBars 460ms steps(2, end) 3; }

@keyframes tearBars {
  0%   { opacity: 0;   transform: translate3d(0, 0, 0); }
  20%  { opacity: 0.9; transform: translate3d(-4px, 2px, 0); }
  45%  { opacity: 0.7; transform: translate3d(5px, -3px, 0); }
  70%  { opacity: 0.9; transform: translate3d(-3px, 1px, 0); }
  100% { opacity: 0;   transform: translate3d(0, 0, 0); }
}

/* Pure CSS: vertical ICE filaments, a drifting scan band and a red core.
   No rAF loop, so it costs nothing on a phone.

   Everything is masked to fall off well before the section boundary. The
   first pass pooled the glow at the bottom edge, where the section's own
   overflow sliced it into a hard horizontal line - it read as a rendering
   fault rather than a design. Nothing touches an edge now, so the wall
   reads as a barrier standing behind the text with no visible end. */
.blackwall {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  mask-image: linear-gradient(180deg,
    transparent 0%,
    #000 20%,
    #000 58%,
    rgba(0, 0, 0, 0.35) 80%,
    transparent 96%);
}

.blackwall__bars,
.blackwall__scan,
.blackwall__glow { position: absolute; inset: 0; }

.blackwall__bars {
  background: repeating-linear-gradient(
    90deg,
    transparent 0 22px,
    var(--wall-12) 22px 23px,
    transparent 23px 64px
  );
  mask-image: radial-gradient(120% 78% at 50% 56%, #000 12%, transparent 76%);
  animation: wallDrift 24s linear infinite;
}

/* the core: brightest behind the headline, gone long before the fold */
.blackwall__glow {
  background:
    radial-gradient(76% 46% at 50% 58%, var(--wall-24) 0%, transparent 70%),
    radial-gradient(38% 24% at 50% 62%, var(--wall-45) 0%, transparent 74%);
  opacity: 0.85;
}

.blackwall__scan {
  background: linear-gradient(
    180deg,
    transparent 0%,
    var(--wall-24) 46%,
    var(--wall-45) 50%,
    var(--wall-24) 54%,
    transparent 100%
  );
  height: 46%;
  filter: blur(14px);
  opacity: 0.5;
  animation: wallScan 11s var(--ease-in) infinite;
}

@keyframes wallDrift {
  from { background-position: 0 0; }
  to   { background-position: 64px 0; }
}
@keyframes wallScan {
  0%   { transform: translate3d(0, -70%, 0); opacity: 0; }
  14%  { opacity: 0.5; }
  82%  { opacity: 0.32; }
  100% { transform: translate3d(0, 170%, 0); opacity: 0; }
}

/* ---------- nav ---------- */

.nav {
  position: sticky;
  top: 0;
  z-index: 40;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-inline: var(--edge);
  background: rgba(5, 6, 10, 0.82);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--hairline);
}

.nav__brand {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  text-decoration: none;
  color: var(--ink);
}
.nav__mark { width: 30px; height: 30px; color: var(--ink); }
.nav__word {
  font-family: var(--display);
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 0.01em;
}

.nav {  }
.nav__cta { --c: 9px; padding: 10px 20px; font-size: 12px; white-space: nowrap; }

/* the pointer sits just under the CTA and nudges up at it */
.nav__point {
  position: absolute;
  top: calc(100% + 4px);
  right: calc(var(--edge) + 52px);
  font-size: 11px;
  line-height: 1;
  color: var(--wall);
  animation: pointNudge 1.8s var(--ease-in) infinite;
}
@keyframes pointNudge {
  0%, 100% { transform: translateY(2px); opacity: 0.55; }
  50%      { transform: translateY(-2px); opacity: 1; }
}

/* ---------- footer ---------- */

.footer {
  border-top: 1px solid var(--hairline);
  background: var(--panel);
  padding: clamp(40px, 7vw, 72px) 0 36px;
  margin-top: clamp(60px, 10vw, 120px);
}
.footer__top {
  display: flex;
  flex-wrap: wrap;
  gap: 32px 48px;
  justify-content: space-between;
  align-items: flex-start;
}
.footer__brand { display: inline-flex; align-items: center; gap: 11px; text-decoration: none; }
.footer__mark { width: 34px; height: 34px; color: var(--wall); }
.footer__word { font-family: var(--display); font-size: 19px; font-weight: 700; }
.footer__tag {
  margin: 8px 0 0;
  font-size: 12px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-40);
}

.footer__links { display: flex; flex-wrap: wrap; gap: 28px 44px; }
.footer__col { display: flex; flex-direction: column; gap: 9px; }
.footer__col > .label { margin-bottom: 4px; }
.footer__col a {
  color: var(--ink-60);
  font-size: 14px;
  text-decoration: none;
  transition: color 160ms var(--ease-out);
}
.footer__col a:hover { color: var(--wall); }

.footer__base {
  margin-top: clamp(28px, 5vw, 48px);
  padding-top: 20px;
  border-top: 1px solid var(--hairline);
  font-size: 12px;
  color: var(--ink-40);
}

/* ---------- entrance ---------- */

.reveal { opacity: 0; transform: translate3d(0, 16px, 0); }
.reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 620ms var(--ease-out) var(--rd, 0ms),
              transform 720ms var(--ease-out) var(--rd, 0ms);
}

/* ---------- motion off ---------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .reveal { opacity: 1; transform: none; }
  .blackwall__scan { display: none; }
  .glitch::before, .glitch::after { display: none; }
  .tearlines { display: none; }
}

/* ---------- code matrix ---------- */

/* The Breach Protocol panel: solid header strip with a glyph and title,
   the grid, and a footer carrying the device legal line, a protocol
   number and a run of figures. Square, because the real one is. */
.matrix {
  position: relative;
  aspect-ratio: 1 / 1;
  display: grid;
  grid-template-rows: auto 1fr auto;
  background: #070204;
  /* a 24% red hairline vanished against the page - the panel needs a real
     edge, so this is a solid 2px rule with a little bleed around it */
  box-shadow:
    inset 0 0 0 2px var(--wall),
    0 0 0 1px rgba(255, 31, 61, 0.35),
    0 0 22px rgba(255, 31, 61, 0.25);
  overflow: hidden;
}

.matrix__head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 7px;
  background: var(--wall);
  color: #120206;
  font-family: var(--display);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
}
.matrix__title { min-width: 0; overflow: hidden; }

.matrix__glyph {
  flex: 0 0 auto;
  width: 12px; height: 12px;
  background:
    linear-gradient(#120206 0 0) 0 0 / 5px 5px no-repeat,
    linear-gradient(#120206 0 0) 7px 0 / 5px 5px no-repeat,
    linear-gradient(#120206 0 0) 0 7px / 5px 5px no-repeat,
    linear-gradient(#120206 0 0) 7px 7px / 5px 5px no-repeat;
}

/* the little tick marks at the head's right, as on the real panel */
.matrix__meta { margin-left: auto; display: inline-flex; gap: 3px; }
.matrix__meta i { width: 9px; height: 2px; background: #120206; opacity: 0.75; }

/* the id floats over the body's top-right corner */
.matrix__id {
  position: absolute;
  top: 26px; right: 7px;
  font-family: var(--display);
  font-size: 7px;
  letter-spacing: 0.1em;
  color: rgba(255, 31, 61, 0.55);
}

.matrix__cells {
  display: grid;
  grid-template-columns: repeat(var(--n, 5), 1fr);
  grid-auto-rows: 1fr;
  align-content: center;
  padding: 4px 5px;
  gap: 1px;
}

.matrix__cells i {
  display: grid;
  place-items: center;
  font-family: var(--display);
  font-size: clamp(7px, 0.78vw, 13px);
  font-weight: 700;
  font-style: normal;
  letter-spacing: 0.06em;
  color: rgba(255, 31, 61, 0.4);
  transition: color 60ms linear, background 60ms linear;
}

/* The row and column the cursor sits on. This is a background band only -
   recolouring the bytes made the highlight read as a text state rather
   than as the axis the cursor is travelling along. */
.matrix__cells i.is-row,
.matrix__cells i.is-col { background: rgba(255, 31, 61, 0.24); }

/* the crossing is a touch stronger */
.matrix__cells i.is-row.is-col { background: rgba(255, 31, 61, 0.36); }

/* taken into the buffer */
.matrix__cells i.is-picked { color: #120206; background: var(--wall); }

/* the cursor itself: a box around the byte being selected */
.matrix__cells i.is-head {
  position: relative;
  background: #fff;
  color: #120206;
  outline: 2px solid #fff;
  outline-offset: 1px;
  box-shadow: 0 0 16px var(--wall-hot), 0 0 34px rgba(255, 31, 61, 0.6);
  z-index: 1;
}

/* ---------- the footer strip ---------- */

.matrix__foot {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  border-top: 1px solid var(--wall-24);
  font-family: var(--display);
  font-size: 5.5px;
  line-height: 1.25;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(255, 31, 61, 0.55);
}

.matrix__fglyph {
  width: 14px; height: 14px;
  background:
    linear-gradient(var(--wall) 0 0) 0 0 / 14px 3px no-repeat,
    linear-gradient(var(--wall) 0 0) 0 5px / 9px 3px no-repeat,
    linear-gradient(var(--wall) 0 0) 0 10px / 14px 3px no-repeat;
}

.matrix__legal {
  min-width: 0;
  max-height: 14px;
  overflow: hidden;
  opacity: 0.6;
}

.matrix__nums { display: grid; grid-template-columns: 1fr 1fr; gap: 0 5px; }
.matrix__nums b { font-weight: 600; opacity: 0.8; }

.matrix__proto { color: var(--wall); font-weight: 700; opacity: 0.85; }

.matrix__strip {
  width: 22px; height: 12px;
  background: repeating-linear-gradient(90deg, var(--wall) 0 2px, transparent 2px 4px);
  opacity: 0.55;
}

/* very small panels drop the chatter rather than squashing it */
@media (max-width: 820px) {
  .matrix__legal, .matrix__nums { display: none; }
  .matrix__foot { grid-template-columns: auto 1fr auto; }
}

/* ---------- scan-line tear (transitions) ---------- */

/* The picture rips: fine scan lines, dozens of displaced half-lines at
   random heights, a red bloom, and a wash that goes opaque early so the
   document swap underneath is never visible. The page barely moves -
   the tear does the work, not a shake. */
.tear {
  position: fixed;
  inset: 0;
  z-index: 95;
  pointer-events: none;
  opacity: 0;
  overflow: hidden;
}
.tear.is-on { opacity: 1; }

.tear__wash,
.tear__bars,
.tear__halves,
.tear__glow { position: absolute; inset: 0; }

/* the backdrop that hides the swap */
.tear__wash {
  background:
    radial-gradient(120% 80% at 50% 50%, rgba(60, 2, 12, 0.96) 0%, rgba(5, 6, 10, 0.99) 68%),
    var(--ground);
  opacity: 0;
}

/* The footage: the datamosh pass, levels normalised and mapped to red,
   black and white. One clip, not four - stacking all of them turned the
   screen to mush and you could not read any single effect. Laid in twice
   at different scales so a one-second loop does not read as a loop. */
.tear__feed {
  position: absolute;
  inset: -6%;
  background: url('../assets/tear.webp') center / cover no-repeat;
  opacity: 0;
  mix-blend-mode: screen;
}
.tear__feed--b {
  inset: -16%;
  transform: scaleX(-1) scaleY(1.22);
  mix-blend-mode: lighten;
}

.tear.is-leaving .tear__feed    { animation: feedIn 400ms steps(4, end) forwards; }
.tear.is-leaving .tear__feed--b { animation: feedInB 400ms steps(3, end) forwards; }

@keyframes feedIn {
  0%   { opacity: 0;    transform: translate3d(0, 0, 0) scale(1.04); }
  16%  { opacity: 1;    transform: translate3d(-10px, 5px, 0) scale(1.09); }
  52%  { opacity: 0.9;  transform: translate3d(8px, -6px, 0) scale(1.02); }
  80%  { opacity: 0.96; transform: translate3d(-4px, 2px, 0) scale(1.07); }
  100% { opacity: 0.78; transform: translate3d(0, 0, 0) scale(1.03); }
}
@keyframes feedInB {
  0%   { opacity: 0;    }
  22%  { opacity: 0.7;  }
  62%  { opacity: 0.42; }
  100% { opacity: 0.58; }
}

/* Fine full-width scan lines. These need an explicit opacity:0 base -
   the layer is otherwise always painted, and the arriving fade then had
   static lines sitting over the page it was revealing. */
.tear__bars {
  background: repeating-linear-gradient(
    180deg,
    rgba(255, 31, 61, 0.42) 0 1px,
    transparent 1px 3px,
    rgba(255, 255, 255, 0.07) 3px 4px,
    transparent 4px 7px);
  mix-blend-mode: screen;
  opacity: 0;
}

/* the half-lines themselves */
.tear__half {
  position: absolute;
  background: var(--wall);
  box-shadow: 0 0 10px rgba(255, 31, 61, 0.8);
  opacity: 0;
  will-change: transform, opacity;
}
.tear__half.is-hot {
  background: #fff;
  box-shadow: 0 0 16px #fff, 0 0 34px var(--wall-hot);
}

.tear.is-leaving .tear__half {
  animation: halfRip 200ms steps(2, end) infinite;
  animation-delay: var(--d, 0ms);
}

@keyframes halfRip {
  0%   { opacity: 0;    transform: translate3d(0, 0, 0) scaleY(1); }
  18%  { opacity: 1;    transform: translate3d(var(--dx, 0), 0, 0) scaleY(1.9); }
  40%  { opacity: 0.25; transform: translate3d(calc(var(--dx, 0) * -0.55), 0, 0) scaleY(0.6); }
  62%  { opacity: 1;    transform: translate3d(calc(var(--dx, 0) * 0.78), 0, 0) scaleY(1.4); }
  84%  { opacity: 0.5;  transform: translate3d(calc(var(--dx, 0) * -0.3), 0, 0) scaleY(1); }
  100% { opacity: 0;    transform: translate3d(0, 0, 0) scaleY(1); }
}

/* the bloom */
.tear__glow {
  background:
    radial-gradient(90% 44% at 50% 38%, rgba(255, 31, 61, 0.4) 0%, transparent 70%),
    radial-gradient(70% 30% at 50% 72%, rgba(255, 77, 95, 0.3) 0%, transparent 72%);
  mix-blend-mode: screen;
  opacity: 0;
}
.tear.is-leaving .tear__glow { animation: tearGlow 300ms ease-out infinite alternate; }

@keyframes tearGlow {
  0%   { opacity: 0.35; }
  100% { opacity: 0.85; }
}

.tear.is-leaving .tear__bars { animation: tearScan 190ms steps(3, end) infinite; }

@keyframes tearScan {
  0%   { transform: translate3d(0, 0, 0);     opacity: 0.9; }
  33%  { transform: translate3d(-5px, 6px, 0); opacity: 0.45; }
  66%  { transform: translate3d(4px, -4px, 0); opacity: 0.85; }
  100% { transform: translate3d(0, 2px, 0);    opacity: 0.6; }
}

/* Leaving: 400ms of glitch, and the wash is solid black by the end of it
   so the document swap happens behind a flat screen. */
.tear.is-leaving .tear__wash { animation: tearWash 340ms ease-in forwards; }
@keyframes tearWash {
  0%   { opacity: 0; }
  70%  { opacity: 0.9; }
  100% { opacity: 1; }
}

/* Arriving is not a glitch - the new page comes up out of flat black, so
   the only thing running is the wash lifting. The feed, half-lines, bars
   and bloom are all scoped to .is-leaving and never fire here. */
.tear.is-arriving .tear__wash {
  background: var(--ground);
  animation: tearClear 1000ms cubic-bezier(0.33, 0, 0.2, 1) forwards;
}
@keyframes tearClear {
  0%   { opacity: 1; }
  38%  { opacity: 0.88; }
  100% { opacity: 0; }
}

/* A nudge, not a shake - the tear carries the violence, the page just
   loses its footing slightly. */
.is-tearing-page { animation: pageJudder 300ms steps(2, end) infinite; }
@keyframes pageJudder {
  0%   { transform: translate3d(0, 0, 0); }
  30%  { transform: translate3d(-2px, 1px, 0); }
  60%  { transform: translate3d(2px, -1px, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .tear { display: none; }
  .is-tearing-page { animation: none; }
}

/* ---------- code matrix ---------- */

.matrix { position: relative; }

.matrix__cells {
  display: grid;
  grid-template-columns: repeat(var(--n, 5), 1fr);
  gap: 2px;
}

.matrix__cells i {
  display: grid;
  place-items: center;
  padding: 2px 0;
  font-family: var(--display);
  font-size: clamp(8px, 0.95vw, 12px);
  font-weight: 700;
  font-style: normal;
  letter-spacing: 0.06em;
  color: rgba(255, 31, 61, 0.42);
  transition: color 90ms linear, background 90ms linear;
}

/* the row or column the cursor is locked to */
.matrix__cells i.is-axis {
  color: var(--wall-hot);
  background: rgba(255, 31, 61, 0.12);
}

/* taken into the buffer */
.matrix__cells i.is-picked {
  color: var(--ground);
  background: var(--wall);
}

/* where the cursor is right now */
.matrix__cells i.is-head {
  background: #fff;
  color: var(--ground);
  box-shadow: 0 0 12px var(--wall-hot);
}

/* ---------- scan-line tear (transitions) ---------- */

/* Horizontal bars that rip across the screen, half-lines displaced
   sideways, red on black. This is the transition - no veil, no mark. */
.tear {
  position: fixed;
  inset: 0;
  z-index: 95;
  pointer-events: none;
  opacity: 0;
  overflow: hidden;
}

.tear__bars,
.tear__slabs,
.tear__wash { position: absolute; inset: -10% 0; }

/* fine scan lines */
.tear__bars {
  background: repeating-linear-gradient(
    180deg,
    rgba(255, 31, 61, 0.55) 0 2px,
    transparent 2px 5px,
    rgba(255, 31, 61, 0.22) 5px 6px,
    transparent 6px 11px);
}

/* fat displaced slabs, the ones that read as the picture tearing */
.tear__slabs {
  background:
    linear-gradient(180deg, transparent 0 6%,  var(--wall) 6% 8%,   transparent 8% 18%),
    linear-gradient(180deg, transparent 0 23%, #fff 23% 23.7%,      transparent 23.7% 31%),
    linear-gradient(180deg, transparent 0 38%, var(--wall-hot) 38% 41%, transparent 41% 49%),
    linear-gradient(180deg, transparent 0 56%, var(--wall) 56% 57%,  transparent 57% 66%),
    linear-gradient(180deg, transparent 0 71%, #fff 71% 71.6%,       transparent 71.6% 79%),
    linear-gradient(180deg, transparent 0 86%, var(--wall) 86% 89%,  transparent 89% 96%);
}

.tear__wash { background: var(--ground); opacity: 0; }

.tear.is-on { opacity: 1; }
.tear.is-on .tear__bars  { animation: tearScan 460ms steps(3, end) infinite; }
.tear.is-on .tear__slabs { animation: tearSlab 300ms steps(2, end) infinite; }
.tear.is-leaving .tear__wash  { animation: tearWash 620ms ease-in forwards; }

/* leaving: the wash goes opaque so the next document can load behind it */
@keyframes tearWash {
  0%   { opacity: 0; }
  55%  { opacity: 0.55; }
  100% { opacity: 1; }
}

@keyframes tearScan {
  0%   { transform: translate3d(0, 0, 0); opacity: 0.9; }
  33%  { transform: translate3d(-9px, 7px, 0); opacity: 0.5; }
  66%  { transform: translate3d(7px, -5px, 0); opacity: 0.85; }
  100% { transform: translate3d(0, 3px, 0); opacity: 0.6; }
}

@keyframes tearSlab {
  0%   { transform: translate3d(0, 0, 0)     scaleY(1); opacity: 0.85; }
  25%  { transform: translate3d(-26px, 0, 0) scaleY(1.4); opacity: 1; }
  50%  { transform: translate3d(18px, 0, 0)  scaleY(0.7); opacity: 0.7; }
  75%  { transform: translate3d(-11px, 0, 0) scaleY(1.2); opacity: 1; }
  100% { transform: translate3d(0, 0, 0)     scaleY(1); opacity: 0.8; }
}

/* arriving: the wash starts opaque and lifts away */
.tear.is-arriving .tear__wash { animation: tearClear 520ms ease-out forwards; }
@keyframes tearClear {
  0%   { opacity: 1; }
  100% { opacity: 0; }
}

/* the page itself judders while the tear runs */
.is-tearing-page { animation: pageJudder 300ms steps(2, end) infinite; }
@keyframes pageJudder {
  0%   { transform: translate3d(0, 0, 0); }
  25%  { transform: translate3d(-6px, 2px, 0); }
  50%  { transform: translate3d(5px, -3px, 0); }
  75%  { transform: translate3d(-3px, 1px, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .tear { display: none; }
  .is-tearing-page { animation: none; }
}

/* ---------- attribution ---------- */

/* A card, built from the same chamfered panel and corner brackets as the
   ones on the home page, sitting under the footer brand. The first pass
   was an 11px caption above the copyright and was far too easy to miss. */
.credit {
  --c: 11px;
  display: inline-block;
  margin-top: clamp(18px, 3vw, 26px);
  max-width: 300px;
}
.credit:hover { background: var(--wall-45); }

.credit__in {
  padding: 15px 18px 16px;
  transition: background 220ms var(--ease-out);
}
.credit:hover .credit__in { background: var(--panel-hi); }

.credit__label { margin: 0 0 9px; font-size: 10px; }

.credit__line {
  margin: 0;
  font-family: var(--display);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.45;
  color: var(--ink-60);
}
.credit__line a {
  /* inline-block so a narrow card breaks between the two titles rather
     than through the middle of one of them */
  display: inline-block;
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--wall-45);
  transition: color 160ms var(--ease-out), border-color 160ms var(--ease-out);
}
.credit__line a:hover { color: var(--wall); border-bottom-color: var(--wall); }
.credit__slash { color: var(--wall); margin: 0 3px; }

@media (max-width: 620px) {
  .credit { max-width: 100%; }
}

/* ---------- nav on small screens ---------- */

/* "Promote Your Business" is long; at 350px it wraps to three lines and
   triples the nav height. The mark alone carries the brand there - the
   wordmark is still in the footer. */
@media (max-width: 620px) {
  .nav__word { display: none; }
  .nav__cta { padding: 9px 14px; font-size: 10.5px; letter-spacing: 0.12em; }
  .nav__point { right: calc(var(--edge) + 34px); }
}
