/* =============================================================================
   Card thumbnail loops — the eight project sketches in this folder (01-08) and
   the two reads' in reads/static/reads/thumbs/ (R1, R2)
   -----------------------------------------------------------------------------
   One small loop per thumbnail. Only the orange marks move; the ink never
   does. The rest state is the static drawing: every keyframe set starts and
   ends on it, and the marks that exist only for the loop (beads, ripples, the
   other ballot ticks, the budget corners) carry opacity="0" in the SVG, so a
   page without this file still shows the drawing exactly.

   The switch is one rule: a mark with .th-anim names its keyframes in --th-k,
   and the loop runs while an ancestor has .th-play (set by thumbs.js) or a
   [data-th-host] is hovered (the no-JS fallback). thumbs.js lets the current
   cycle finish on leave; removing it mid-cycle would snap (see thumbs.js).

   Shared by all ten, so they read as one set: one cycle (--th-cycle), one
   easing (--th-ease, applied per keyframe segment, so every move eases in and
   out of its holds), and the same gate: hover-capable pointers only, never
   under prefers-reduced-motion. Motion starts the moment a card is hovered:
   every loop leaves the rest pose at 0% on the ease-out half of the same
   curve (cubic-bezier(0, 0, .55, 1)), so something orange visibly moves
   within the first ~100 ms, and the rest hold sits at the end of the cycle.

   Only transform, opacity and stroke-dashoffset are animated. Shape changes
   (the mouth in 04) crossfade separate shapes; nothing animates `d`. A CSS
   transform replaces an SVG transform="" attribute, so no moving mark has one.
   Origins are in the drawing's own units (transform-box: view-box, viewBox
   0 0 320 200). Keyframe names, classes and origins: th-<project>-<mark>.

   Allowed here, and only here (core/tests.py CleanStyleTests): the dashoffset
   draw-on of 01's mercury, 02, 03 and 05, and R2's pulses (a short dash each,
   its length set in the SVG, so it takes no .th-dash). Dashed marks (.th-dash) take
   pathLength="100" in the SVG and the 100/200 pattern below while playing, so
   their offsets read as percentages of the stroke: 0 drawn, -102 to -198
   gone, -300 drawn again (positive offsets pull the end back to the start).

   Chromium puts marks with transform or opacity animations on compositor
   layers, which can soften a drawing while it plays. Measured: with 01's
   mercury scaled (a transform) under the bulb, the whole of 01 went soft
   until the loop stopped, so the mercury is dash-animated instead. After
   adding a moving mark, compare a paused frame at the start of the cycle
   with the rest drawing in Chromium: today only anti-aliasing noise on a
   few edge pixels differs (WebKit and Firefox: identical).
   ============================================================================= */

.pe-sketch {
    --th-cycle: 3.6s;
    --th-ease: cubic-bezier(.45, 0, .55, 1);
}
.pe-sketch .th-anim { transform-box: view-box; }

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
    .th-play .th-anim,
    [data-th-host]:hover .th-anim {
        animation: var(--th-k) var(--th-cycle) var(--th-ease) infinite;
    }
    .th-play .th-dash,
    [data-th-host]:hover .th-dash { stroke-dasharray: 100 200; }
}

/* Belt and braces: nothing in a thumbnail moves under reduced motion, even if a
   rule outside the gate above ever starts an animation. */
@media (prefers-reduced-motion: reduce) {
    .pe-sketch .th-anim { animation: none !important; }
    .pe-sketch .th-dash { stroke-dasharray: none !important; }
}


/* -----------------------------------------------------------------------------
   01 Climate KOLs on Twitter — the mercury drops and climbs back, a retweet
   leaves the phone along both orange ties, and each key opinion leader lights
   up as it arrives (the beads start under the phone and end under the nodes).
   -------------------------------------------------------------------------- */
.th-kols-mercury { --th-k: th-kols-mercury; }
.th-kols-bead-a  { --th-k: th-kols-bead-a; }
.th-kols-bead-b  { --th-k: th-kols-bead-b; }
.th-kols-ripple-a { --th-k: th-kols-ripple-a; transform-origin: 64px 100px; }
.th-kols-ripple-b { --th-k: th-kols-ripple-b; transform-origin: 242px 54px; }
.th-kols-lit     { --th-k: th-kols-lit; }

@keyframes th-kols-mercury {
    0%    { stroke-dashoffset: 0; animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    9%    { stroke-dashoffset: 60; }
    24%, 100% { stroke-dashoffset: 0; }
}
@keyframes th-kols-bead-a {
    0%, 17% { transform: translate(0, 0); opacity: 0; }
    18% { transform: translate(0, 0); opacity: 1; }
    45% { transform: translate(-86px, 0); opacity: 1; }
    46%, 100% { transform: translate(-86px, 0); opacity: 0; }
}
@keyframes th-kols-bead-b {
    0%, 21% { transform: translate(0, 0); opacity: 0; }
    22% { transform: translate(0, 0); opacity: 1; }
    49% { transform: translate(71.5px, -21.4px); opacity: 1; }
    50%, 100% { transform: translate(71.5px, -21.4px); opacity: 0; }
}
/* A ripple starts at .6 of its radius, under its node, so it rises out of it. */
@keyframes th-kols-ripple-a {
    0%, 44% { transform: scale(.6); opacity: 0; }
    45% { transform: scale(.6); opacity: .9; }
    71%, 100% { transform: scale(1.4); opacity: 0; }
}
@keyframes th-kols-ripple-b {
    0%, 48% { transform: scale(.6); opacity: 0; }
    49% { transform: scale(.6); opacity: .9; }
    75%, 100% { transform: scale(1.35); opacity: 0; }
}
@keyframes th-kols-lit {
    0%, 43% { opacity: 0; }
    49%, 66% { opacity: 1; }
    82%, 100% { opacity: 0; }
}


/* -----------------------------------------------------------------------------
   02 UNGA Voting Coincidence — the tick lifts off YES and is drawn in NO, then
   ABSTAIN, then YES again. Each box holds its own tick; one is erased (the
   dash runs off its end) while the next is drawn.
   -------------------------------------------------------------------------- */
.th-unga-yes     { --th-k: th-unga-yes; }
.th-unga-no      { --th-k: th-unga-no; }
.th-unga-abstain { --th-k: th-unga-abstain; }

@keyframes th-unga-yes {
    0%    { stroke-dashoffset: 0; animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    12% { stroke-dashoffset: -102; }
    72% { stroke-dashoffset: -198; }
    86%, 100% { stroke-dashoffset: -300; }
}
@keyframes th-unga-no {
    0%, 6% { stroke-dashoffset: -198; opacity: 1; }
    20%, 34% { stroke-dashoffset: -300; opacity: 1; }
    46%, 100% { stroke-dashoffset: -402; opacity: 1; }
}
@keyframes th-unga-abstain {
    0%, 40% { stroke-dashoffset: -198; opacity: 1; }
    54%, 64% { stroke-dashoffset: -300; opacity: 1; }
    76%, 100% { stroke-dashoffset: -402; opacity: 1; }
}


/* -----------------------------------------------------------------------------
   03 UNGD Linguistic Evolution — the rising word's line is wiped off, then drawn
   again across the years with its dot leading, like a frequency being
   counted year by year. The draw is sampled (linear between samples of the
   shared easing) so the dot stays on the tip of the line.
   -------------------------------------------------------------------------- */
.th-ungd-line { --th-k: th-ungd-line; }
.th-ungd-dot  { --th-k: th-ungd-dot; }

@keyframes th-ungd-line {
    0%      { stroke-dashoffset: 0; animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    10%     { stroke-dashoffset: -102; }
    16%     { stroke-dashoffset: -198; }
    20% { stroke-dashoffset: -200; animation-timing-function: linear; }
    23.12% { stroke-dashoffset: -200.67; animation-timing-function: linear; }
    26.25% { stroke-dashoffset: -202.79; animation-timing-function: linear; }
    29.38% { stroke-dashoffset: -206.53; animation-timing-function: linear; }
    32.5% { stroke-dashoffset: -212.02; animation-timing-function: linear; }
    35.62% { stroke-dashoffset: -219.32; animation-timing-function: linear; }
    38.75% { stroke-dashoffset: -228.35; animation-timing-function: linear; }
    41.88% { stroke-dashoffset: -238.78; animation-timing-function: linear; }
    45% { stroke-dashoffset: -250; animation-timing-function: linear; }
    48.12% { stroke-dashoffset: -261.22; animation-timing-function: linear; }
    51.25% { stroke-dashoffset: -271.65; animation-timing-function: linear; }
    54.38% { stroke-dashoffset: -280.68; animation-timing-function: linear; }
    57.5% { stroke-dashoffset: -287.98; animation-timing-function: linear; }
    60.62% { stroke-dashoffset: -293.47; animation-timing-function: linear; }
    63.75% { stroke-dashoffset: -297.21; animation-timing-function: linear; }
    66.88% { stroke-dashoffset: -299.33; animation-timing-function: linear; }
    70%, 100% { stroke-dashoffset: -300; opacity: 1; }
}
@keyframes th-ungd-dot {
    0%, 7%  { transform: translate(0, 0); opacity: 1; }
    10%     { transform: translate(0, 0); opacity: 0; }
    16%     { transform: translate(-134px, 58px); opacity: 0; }
    20% { transform: translate(-134px, 58px); opacity: 1; animation-timing-function: linear; }
    23.12% { transform: translate(-133.01px, 57.94px); animation-timing-function: linear; }
    26.25% { transform: translate(-129.88px, 57.71px); animation-timing-function: linear; }
    29.38% { transform: translate(-124.37px, 57.17px); animation-timing-function: linear; }
    32.5% { transform: translate(-116.33px, 56.01px); animation-timing-function: linear; }
    35.62% { transform: translate(-105.75px, 53.75px); animation-timing-function: linear; }
    38.75% { transform: translate(-93.04px, 49.63px); animation-timing-function: linear; }
    41.88% { transform: translate(-79.23px, 42.77px); animation-timing-function: linear; }
    45% { transform: translate(-65.23px, 33.82px); animation-timing-function: linear; }
    48.12% { transform: translate(-51.29px, 24.77px); animation-timing-function: linear; }
    51.25% { transform: translate(-38.4px, 16.27px); animation-timing-function: linear; }
    54.38% { transform: translate(-26.9px, 9.46px); animation-timing-function: linear; }
    57.5% { transform: translate(-17.05px, 5px); animation-timing-function: linear; }
    60.62% { transform: translate(-9.37px, 2.35px); animation-timing-function: linear; }
    63.75% { transform: translate(-4.03px, .89px); animation-timing-function: linear; }
    66.88% { transform: translate(-.97px, .2px); animation-timing-function: linear; }
    70%, 100% { transform: translate(0, 0); opacity: 1; }
}


/* -----------------------------------------------------------------------------
   04 Faces of Diplomacy — the face starts composed and gets angry: the marks
   beside the head go one after another, brows level and the frown crossfades
   (flattening as it goes) into a smile; after a beat the brows draw down, the
   smile turns back into the frown and the marks return one after another.
   -------------------------------------------------------------------------- */
.th-faces-brow-l { --th-k: th-faces-brow-l; transform-origin: 191.5px 79.5px; }
.th-faces-brow-r { --th-k: th-faces-brow-r; transform-origin: 208.5px 79.5px; }
.th-faces-frown  { --th-k: th-faces-frown; transform-origin: 200px 98.25px; }
.th-faces-smile  { --th-k: th-faces-smile; transform-origin: 200px 98.25px; }
.th-faces-mark-1 { --th-k: th-faces-mark-1; }
.th-faces-mark-2 { --th-k: th-faces-mark-2; }
.th-faces-mark-3 { --th-k: th-faces-mark-3; }

@keyframes th-faces-brow-l {
    0%      { transform: translate(0, 0) rotate(0deg); animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    12%, 40% { transform: translate(0, -1px) rotate(-18deg); }
    52%, 100% { transform: translate(0, 0) rotate(0deg); }
}
@keyframes th-faces-brow-r {
    0%      { transform: translate(0, 0) rotate(0deg); animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    12%, 40% { transform: translate(0, -1px) rotate(18deg); }
    52%, 100% { transform: translate(0, 0) rotate(0deg); }
}
@keyframes th-faces-frown {
    0%, 4% { transform: scaleY(1); opacity: 1; }
    11%, 44% { transform: scaleY(.4); opacity: 0; }
    51%, 100% { transform: scaleY(1); opacity: 1; }
}
@keyframes th-faces-smile {
    0%, 8% { transform: scaleY(.4); opacity: 0; }
    15%, 40% { transform: scaleY(1); opacity: 1; }
    47%, 100% { transform: scaleY(.4); opacity: 0; }
}
/* Each mark leaves outward and comes back in from the head's side. */
@keyframes th-faces-mark-1 {
    0%    { transform: translate(0, 0); opacity: 1; animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    7% { transform: translate(1.7px, -1px); opacity: 0; }
    49% { transform: translate(-1.7px, 1px); opacity: 0; }
    57%, 100% { transform: translate(0, 0); opacity: 1; }
}
@keyframes th-faces-mark-2 {
    0%, 3% { transform: translate(0, 0); opacity: 1; }
    10% { transform: translate(2px, 0); opacity: 0; }
    56% { transform: translate(-2px, 0); opacity: 0; }
    64%, 100% { transform: translate(0, 0); opacity: 1; }
}
@keyframes th-faces-mark-3 {
    0%, 6% { transform: translate(0, 0); opacity: 1; }
    13% { transform: translate(1.7px, 1px); opacity: 0; }
    63% { transform: translate(-1.7px, -1px); opacity: 0; }
    71%, 100% { transform: translate(0, 0); opacity: 1; }
}


/* -----------------------------------------------------------------------------
   05 Mapping Forced Migration — the flow that crossed is pushed back along
   its own path (the arrowhead still facing the border it was crossing), then
   crosses again. Sampled like 03 so the arrowhead rides the tip and turns
   with the curve; it pivots on the end of the line (213, 138).
   -------------------------------------------------------------------------- */
.th-fm-flow { --th-k: th-fm-flow; }
.th-fm-head { --th-k: th-fm-head; transform-origin: 213px 138px; }

@keyframes th-fm-flow {
    0%      { stroke-dashoffset: 0; animation-timing-function: linear; }
    2.4%    { stroke-dashoffset: 10.05; animation-timing-function: linear; }
    4.8%    { stroke-dashoffset: 19.18; animation-timing-function: linear; }
    7.2%    { stroke-dashoffset: 27.54; animation-timing-function: linear; }
    9.6%    { stroke-dashoffset: 35.14; animation-timing-function: linear; }
    12%     { stroke-dashoffset: 41.93; animation-timing-function: linear; }
    14.4%   { stroke-dashoffset: 47.84; animation-timing-function: linear; }
    16.8%   { stroke-dashoffset: 52.77; animation-timing-function: linear; }
    19.2%   { stroke-dashoffset: 56.58; animation-timing-function: linear; }
    21.6%   { stroke-dashoffset: 59.09; animation-timing-function: linear; }
    24%, 38% { stroke-dashoffset: 60; animation-timing-function: linear; }
    41.14%  { stroke-dashoffset: 59.47; animation-timing-function: linear; }
    44.29%  { stroke-dashoffset: 57.79; animation-timing-function: linear; }
    47.43%  { stroke-dashoffset: 54.81; animation-timing-function: linear; }
    50.57%  { stroke-dashoffset: 50.42; animation-timing-function: linear; }
    53.71%  { stroke-dashoffset: 44.64; animation-timing-function: linear; }
    56.86%  { stroke-dashoffset: 37.67; animation-timing-function: linear; }
    60%     { stroke-dashoffset: 30; animation-timing-function: linear; }
    63.14%  { stroke-dashoffset: 22.33; animation-timing-function: linear; }
    66.29%  { stroke-dashoffset: 15.36; animation-timing-function: linear; }
    69.43%  { stroke-dashoffset: 9.58; animation-timing-function: linear; }
    72.57%  { stroke-dashoffset: 5.19; animation-timing-function: linear; }
    75.71%  { stroke-dashoffset: 2.21; animation-timing-function: linear; }
    78.86%  { stroke-dashoffset: .53; animation-timing-function: linear; }
    82%, 100% { stroke-dashoffset: 0; }
}
@keyframes th-fm-head {
    0%      { transform: translate(0px, 0px) rotate(0deg); animation-timing-function: linear; }
    2.4%    { transform: translate(-13.04px, -.1px) rotate(4.5deg); animation-timing-function: linear; }
    4.8%    { transform: translate(-24.84px, -1.11px) rotate(8.74deg); animation-timing-function: linear; }
    7.2%    { transform: translate(-35.56px, -2.8px) rotate(12.66deg); animation-timing-function: linear; }
    9.6%    { transform: translate(-45.18px, -4.97px) rotate(16.22deg); animation-timing-function: linear; }
    12%     { transform: translate(-53.64px, -7.39px) rotate(19.3deg); animation-timing-function: linear; }
    14.4%   { transform: translate(-60.9px, -9.87px) rotate(21.89deg); animation-timing-function: linear; }
    16.8%   { transform: translate(-66.87px, -12.19px) rotate(24.02deg); animation-timing-function: linear; }
    19.2%   { transform: translate(-71.42px, -14.13px) rotate(25.7deg); animation-timing-function: linear; }
    21.6%   { transform: translate(-74.38px, -15.48px) rotate(26.73deg); animation-timing-function: linear; }
    24%, 38% { transform: translate(-75.45px, -15.98px) rotate(27.14deg); animation-timing-function: linear; }
    41.14%  { transform: translate(-74.83px, -15.69px) rotate(26.83deg); animation-timing-function: linear; }
    44.29%  { transform: translate(-72.85px, -14.77px) rotate(26.22deg); animation-timing-function: linear; }
    47.43%  { transform: translate(-69.31px, -13.21px) rotate(24.97deg); animation-timing-function: linear; }
    50.57%  { transform: translate(-64.04px, -11.06px) rotate(23.07deg); animation-timing-function: linear; }
    53.71%  { transform: translate(-56.99px, -8.49px) rotate(20.49deg); animation-timing-function: linear; }
    56.86%  { transform: translate(-48.35px, -5.82px) rotate(17.32deg); animation-timing-function: linear; }
    60%     { transform: translate(-38.69px, -3.44px) rotate(13.77deg); animation-timing-function: linear; }
    63.14%  { transform: translate(-28.9px, -1.66px) rotate(10.2deg); animation-timing-function: linear; }
    66.29%  { transform: translate(-19.92px, -.58px) rotate(6.95deg); animation-timing-function: linear; }
    69.43%  { transform: translate(-12.43px, -.08px) rotate(4.28deg); animation-timing-function: linear; }
    72.57%  { transform: translate(-6.74px, .08px) rotate(2.29deg); animation-timing-function: linear; }
    75.71%  { transform: translate(-2.87px, .07px) rotate(.98deg); animation-timing-function: linear; }
    78.86%  { transform: translate(-.69px, .02px) rotate(.22deg); animation-timing-function: linear; }
    82%, 100% { transform: translate(0px, 0px) rotate(0deg); }
}


/* -----------------------------------------------------------------------------
   06 AI Citation Network — the hub fires and a signal spreads through the
   orange bloc, tie by tie: hub -> two neighbours -> the next two -> the last
   node, each lit as it arrives (beads start and end under the nodes).
   -------------------------------------------------------------------------- */
.th-cite-hub { --th-k: th-cite-hub; transform-origin: 202.8px 85.2px; }
.th-cite-ha { --th-k: th-cite-ha; }
.th-cite-hb { --th-k: th-cite-hb; }
.th-cite-ac { --th-k: th-cite-ac; }
.th-cite-bd { --th-k: th-cite-bd; }
.th-cite-ce { --th-k: th-cite-ce; }
.th-cite-de { --th-k: th-cite-de; }
.th-cite-a, .th-cite-b { --th-k: th-cite-lit-1; }
.th-cite-c, .th-cite-d { --th-k: th-cite-lit-2; }
.th-cite-e { --th-k: th-cite-lit-3; }

@keyframes th-cite-hub {
    0%    { transform: scale(1); animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    6% { transform: scale(1.25); }
    16%, 100% { transform: scale(1); }
}
@keyframes th-cite-ha {
    0%, 5% { transform: translate(0, 0); opacity: 0; }
    6% { transform: translate(0, 0); opacity: 1; }
    24% { transform: translate(12px, -28.8px); opacity: 1; }
    25%, 100% { transform: translate(12px, -28.8px); opacity: 0; }
}
@keyframes th-cite-hb {
    0%, 5% { transform: translate(0, 0); opacity: 0; }
    6% { transform: translate(0, 0); opacity: 1; }
    24% { transform: translate(26.4px, 19.2px); opacity: 1; }
    25%, 100% { transform: translate(26.4px, 19.2px); opacity: 0; }
}
@keyframes th-cite-ac {
    0%, 25% { transform: translate(0, 0); opacity: 0; }
    26% { transform: translate(0, 0); opacity: 1; }
    44% { transform: translate(7.2px, -21.6px); opacity: 1; }
    45%, 100% { transform: translate(7.2px, -21.6px); opacity: 0; }
}
@keyframes th-cite-bd {
    0%, 25% { transform: translate(0, 0); opacity: 0; }
    26% { transform: translate(0, 0); opacity: 1; }
    44% { transform: translate(31.2px, -14.4px); opacity: 1; }
    45%, 100% { transform: translate(31.2px, -14.4px); opacity: 0; }
}
@keyframes th-cite-ce {
    0%, 45% { transform: translate(0, 0); opacity: 0; }
    46% { transform: translate(0, 0); opacity: 1; }
    64% { transform: translate(26.4px, 24px); opacity: 1; }
    65%, 100% { transform: translate(26.4px, 24px); opacity: 0; }
}
@keyframes th-cite-de {
    0%, 45% { transform: translate(0, 0); opacity: 0; }
    46% { transform: translate(0, 0); opacity: 1; }
    64% { transform: translate(-12px, -31.2px); opacity: 1; }
    65%, 100% { transform: translate(-12px, -31.2px); opacity: 0; }
}
@keyframes th-cite-lit-1 {
    0%, 21% { opacity: 0; }
    27%, 40% { opacity: 1; }
    54%, 100% { opacity: 0; }
}
@keyframes th-cite-lit-2 {
    0%, 41% { opacity: 0; }
    47%, 58% { opacity: 1; }
    72%, 100% { opacity: 0; }
}
@keyframes th-cite-lit-3 {
    0%, 61% { opacity: 0; }
    67%, 74% { opacity: 1; }
    88%, 100% { opacity: 0; }
}


/* -----------------------------------------------------------------------------
   07 Geopolitical Indicators — the falling line ticks like a live feed: up,
   down, up, and back to its fall. Jointed at its middle point: the first
   segment turns on the line's start, the last segment and the arrowhead on
   the joint, so the line never breaks.
   -------------------------------------------------------------------------- */
.th-ind-arm  { --th-k: th-ind-arm; transform-origin: 226px 124px; }
.th-ind-hand { --th-k: th-ind-hand; transform-origin: 254px 152px; }

@keyframes th-ind-arm {
    0%    { transform: rotate(0deg); animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    10%, 20% { transform: rotate(-6deg); }
    32%, 42% { transform: rotate(1deg); }
    54%, 64% { transform: rotate(-3deg); }
    78%, 100% { transform: rotate(0deg); }
}
@keyframes th-ind-hand {
    0%    { transform: rotate(0deg); animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    10%, 20% { transform: rotate(-26deg); }
    32%, 42% { transform: rotate(3deg); }
    54%, 64% { transform: rotate(-14deg); }
    78%, 100% { transform: rotate(0deg); }
}


/* -----------------------------------------------------------------------------
   08 Anatomy of a Budget — four corners settle on the borrowing tile, lift
   off it and frame health, then housing, then return and fade back into the
   dashed corners: the borrowed money being allocated. Each corner moves on
   its own, so the frame changes size without scaling the stroke.
   -------------------------------------------------------------------------- */
.th-bud-tl { --th-k: th-bud-tl; }
.th-bud-tr { --th-k: th-bud-tr; }
.th-bud-bl { --th-k: th-bud-bl; }
.th-bud-br { --th-k: th-bud-br; }

@keyframes th-bud-tl {
    0%       { transform: translate(0, 0); opacity: 0; animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    5%       { opacity: 1; }
    18%, 32% { transform: translate(-2px, -70px); }
    46%, 58% { transform: translate(59px, -71px); }
    76%      { transform: translate(0, 0); opacity: 1; }
    86%, 100% { transform: translate(0, 0); opacity: 0; }
}
@keyframes th-bud-tr {
    0%       { transform: translate(0, 0); opacity: 0; animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    5%       { opacity: 1; }
    18%, 32% { transform: translate(-48.5px, -69px); }
    46%, 58% { transform: translate(2px, -70px); }
    76%      { transform: translate(0, 0); opacity: 1; }
    86%, 100% { transform: translate(0, 0); opacity: 0; }
}
@keyframes th-bud-bl {
    0%       { transform: translate(0, 0); opacity: 0; animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    5%       { opacity: 1; }
    18%, 32% { transform: translate(-2px, -70.5px); }
    46%, 58% { transform: translate(59px, -70.5px); }
    76%      { transform: translate(0, 0); opacity: 1; }
    86%, 100% { transform: translate(0, 0); opacity: 0; }
}
@keyframes th-bud-br {
    0%       { transform: translate(0, 0); opacity: 0; animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    5%       { opacity: 1; }
    18%, 32% { transform: translate(-48.5px, -69.5px); }
    46%, 58% { transform: translate(2px, -69.5px); }
    76%      { transform: translate(0, 0); opacity: 1; }
    86%, 100% { transform: translate(0, 0); opacity: 0; }
}


/* -----------------------------------------------------------------------------
   R1 AI Derailment Log (reads/static/reads/thumbs/) — the locomotive runs: both
   cranks turn twice and the coupling rod circles with them (each of its points
   moves on the crank's circle, so it stays level), slowing into the halt; each
   smoke puff drifts off and fades while a new one rises out of the chimney
   into its place. The rod is sampled from the cranks' easing so it stays on
   the pins; the puffs ride one Catmull-Rom curve through the smoke.
   -------------------------------------------------------------------------- */
.th-rail-crank-a { --th-k: th-rail-crank; transform-origin: 132px 147px; }
.th-rail-crank-b { --th-k: th-rail-crank; transform-origin: 168px 147px; }
.th-rail-rod     { --th-k: th-rail-rod; }
.th-rail-puff-a { --th-k: th-rail-puff-a; transform-origin: 193px 53px; }
.th-rail-puff-b { --th-k: th-rail-puff-b; transform-origin: 180px 38px; }
.th-rail-puff-c { --th-k: th-rail-puff-c; transform-origin: 160px 26px; }

@keyframes th-rail-crank {
    0%    { transform: rotate(0deg); animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    70%, 100% { transform: rotate(720deg); }
}
@keyframes th-rail-rod {
    0% { transform: translate(0px, 0px); animation-timing-function: linear; }
    1.46% { transform: translate(-2.66px, -2.5px); animation-timing-function: linear; }
    2.92% { transform: translate(-3.92px, -5.81px); animation-timing-function: linear; }
    4.37% { transform: translate(-3.65px, -9.25px); animation-timing-function: linear; }
    5.83% { transform: translate(-2.01px, -12.21px); animation-timing-function: linear; }
    7.29% { transform: translate(0.66px, -14.2px); animation-timing-function: linear; }
    8.75% { transform: translate(3.84px, -14.93px); animation-timing-function: linear; }
    10.21% { transform: translate(7px, -14.35px); animation-timing-function: linear; }
    11.67% { transform: translate(9.63px, -12.62px); animation-timing-function: linear; }
    13.12% { transform: translate(11.36px, -10.06px); animation-timing-function: linear; }
    14.58% { transform: translate(12px, -7.08px); animation-timing-function: linear; }
    16.04% { transform: translate(11.5px, -4.14px); animation-timing-function: linear; }
    17.5% { transform: translate(9.99px, -1.63px); animation-timing-function: linear; }
    18.96% { transform: translate(7.73px, 0.15px); animation-timing-function: linear; }
    20.42% { transform: translate(5.04px, 1px); animation-timing-function: linear; }
    21.88% { transform: translate(2.28px, 0.88px); animation-timing-function: linear; }
    23.33% { transform: translate(-0.23px, -0.14px); animation-timing-function: linear; }
    24.79% { transform: translate(-2.22px, -1.9px); animation-timing-function: linear; }
    26.25% { transform: translate(-3.51px, -4.16px); animation-timing-function: linear; }
    27.71% { transform: translate(-4px, -6.66px); animation-timing-function: linear; }
    29.17% { transform: translate(-3.69px, -9.12px); animation-timing-function: linear; }
    30.62% { transform: translate(-2.68px, -11.33px); animation-timing-function: linear; }
    32.08% { transform: translate(-1.1px, -13.09px); animation-timing-function: linear; }
    33.54% { transform: translate(0.87px, -14.29px); animation-timing-function: linear; }
    35% { transform: translate(3.04px, -14.87px); animation-timing-function: linear; }
    36.46% { transform: translate(5.22px, -14.83px); animation-timing-function: linear; }
    37.92% { transform: translate(7.25px, -14.24px); animation-timing-function: linear; }
    39.38% { transform: translate(9.01px, -13.17px); animation-timing-function: linear; }
    40.83% { transform: translate(10.39px, -11.74px); animation-timing-function: linear; }
    42.29% { transform: translate(11.35px, -10.09px); animation-timing-function: linear; }
    43.75% { transform: translate(11.88px, -8.32px); animation-timing-function: linear; }
    45.21% { transform: translate(11.99px, -6.55px); animation-timing-function: linear; }
    46.67% { transform: translate(11.73px, -4.87px); animation-timing-function: linear; }
    48.12% { transform: translate(11.16px, -3.36px); animation-timing-function: linear; }
    49.58% { transform: translate(10.34px, -2.05px); animation-timing-function: linear; }
    51.04% { transform: translate(9.35px, -0.98px); animation-timing-function: linear; }
    52.5% { transform: translate(8.25px, -0.15px); animation-timing-function: linear; }
    53.96% { transform: translate(7.1px, 0.44px); animation-timing-function: linear; }
    55.42% { transform: translate(5.97px, 0.83px); animation-timing-function: linear; }
    56.88% { transform: translate(4.88px, 1.02px); animation-timing-function: linear; }
    58.33% { transform: translate(3.87px, 1.07px); animation-timing-function: linear; }
    59.79% { transform: translate(2.97px, 1.01px); animation-timing-function: linear; }
    61.25% { transform: translate(2.18px, 0.86px); animation-timing-function: linear; }
    62.71% { transform: translate(1.51px, 0.67px); animation-timing-function: linear; }
    64.17% { transform: translate(0.97px, 0.47px); animation-timing-function: linear; }
    65.62% { transform: translate(0.54px, 0.29px); animation-timing-function: linear; }
    67.08% { transform: translate(0.24px, 0.13px); animation-timing-function: linear; }
    68.54% { transform: translate(0.06px, 0.04px); animation-timing-function: linear; }
    70%, 100% { transform: translate(0px, 0px); }
}
@keyframes th-rail-puff-a {
    0%, 8% { transform: translate(0px, 0px) scale(1); opacity: 1; animation-timing-function: linear; }
    9.75% { transform: translate(-0.28px, -0.37px) scale(1.014); opacity: 0.972; animation-timing-function: linear; }
    11.5% { transform: translate(-1.24px, -1.64px) scale(1.06); opacity: 0.88; animation-timing-function: linear; }
    13.25% { transform: translate(-3.05px, -4.05px) scale(1.142); opacity: 0.717; animation-timing-function: linear; }
    15% { transform: translate(-5.69px, -7.44px) scale(1.25); opacity: 0.5; animation-timing-function: linear; }
    16.75% { transform: translate(-8.63px, -10.85px) scale(1.358); opacity: 0.283; animation-timing-function: linear; }
    18.5% { transform: translate(-11.07px, -13.31px) scale(1.44); opacity: 0.12; animation-timing-function: linear; }
    20.25% { transform: translate(-12.54px, -14.62px) scale(1.486); opacity: 0.028; animation-timing-function: linear; }
    22% { transform: translate(-13px, -15px) scale(1.5); opacity: 0; animation-timing-function: linear; }
    22.5%, 28% { transform: translate(7px, 11px) scale(0.5); opacity: 0; animation-timing-function: linear; }
    30.67% { transform: translate(6.96px, 10.93px) scale(0.506); opacity: 0.333; animation-timing-function: linear; }
    33.33% { transform: translate(6.81px, 10.7px) scale(0.525); opacity: 0.667; animation-timing-function: linear; }
    36% { transform: translate(6.52px, 10.22px) scale(0.56); opacity: 1; animation-timing-function: linear; }
    38.67% { transform: translate(6.03px, 9.38px) scale(0.611); opacity: 1; animation-timing-function: linear; }
    41.33% { transform: translate(5.3px, 8.1px) scale(0.676); opacity: 1; animation-timing-function: linear; }
    44% { transform: translate(4.31px, 6.44px) scale(0.75); opacity: 1; animation-timing-function: linear; }
    46.67% { transform: translate(3.19px, 4.61px) scale(0.824); opacity: 1; animation-timing-function: linear; }
    49.33% { transform: translate(2.09px, 2.91px) scale(0.889); opacity: 1; animation-timing-function: linear; }
    52% { transform: translate(1.16px, 1.58px) scale(0.94); opacity: 1; animation-timing-function: linear; }
    54.67% { transform: translate(0.5px, 0.67px) scale(0.975); opacity: 1; animation-timing-function: linear; }
    57.33% { transform: translate(0.12px, 0.16px) scale(0.994); opacity: 1; animation-timing-function: linear; }
    60%, 100% { transform: translate(0px, 0px) scale(1); opacity: 1; }
}
@keyframes th-rail-puff-b {
    0%, 4% { transform: translate(0px, 0px) scale(1); opacity: 1; animation-timing-function: linear; }
    5.75% { transform: translate(-0.46px, -0.38px) scale(1.009); opacity: 0.972; animation-timing-function: linear; }
    7.5% { transform: translate(-2.05px, -1.61px) scale(1.04); opacity: 0.88; animation-timing-function: linear; }
    9.25% { transform: translate(-5.05px, -3.74px) scale(1.094); opacity: 0.717; animation-timing-function: linear; }
    11% { transform: translate(-9.31px, -6.44px) scale(1.167); opacity: 0.5; animation-timing-function: linear; }
    12.75% { transform: translate(-13.84px, -8.98px) scale(1.239); opacity: 0.283; animation-timing-function: linear; }
    14.5% { transform: translate(-17.37px, -10.76px) scale(1.293); opacity: 0.12; animation-timing-function: linear; }
    16.25% { transform: translate(-19.39px, -11.72px) scale(1.324); opacity: 0.028; animation-timing-function: linear; }
    18% { transform: translate(-20px, -12px) scale(1.333); opacity: 0; animation-timing-function: linear; }
    18.5%, 22% { transform: translate(20px, 26px) scale(0.333); opacity: 0; animation-timing-function: linear; }
    24.83% { transform: translate(19.91px, 25.86px) scale(0.341); opacity: 0.333; animation-timing-function: linear; }
    27.67% { transform: translate(19.6px, 25.35px) scale(0.367); opacity: 0.667; animation-timing-function: linear; }
    30.5% { transform: translate(18.93px, 24.21px) scale(0.413); opacity: 1; animation-timing-function: linear; }
    33.33% { transform: translate(17.71px, 22.1px) scale(0.481); opacity: 1; animation-timing-function: linear; }
    36.17% { transform: translate(15.73px, 18.89px) scale(0.568); opacity: 1; animation-timing-function: linear; }
    39% { transform: translate(13px, 15px) scale(0.667); opacity: 1; animation-timing-function: linear; }
    41.83% { transform: translate(9.8px, 10.75px) scale(0.765); opacity: 1; animation-timing-function: linear; }
    44.67% { transform: translate(6.57px, 6.66px) scale(0.852); opacity: 1; animation-timing-function: linear; }
    47.5% { transform: translate(3.74px, 3.48px) scale(0.92); opacity: 1; animation-timing-function: linear; }
    50.33% { transform: translate(1.64px, 1.42px) scale(0.966); opacity: 1; animation-timing-function: linear; }
    53.17% { transform: translate(0.4px, 0.33px) scale(0.992); opacity: 1; animation-timing-function: linear; }
    56%, 100% { transform: translate(0px, 0px) scale(1); opacity: 1; }
}
@keyframes th-rail-puff-c {
    0% { transform: translate(0px, 0px) scale(1); opacity: 1; animation-timing-function: linear; }
    1.75% { transform: translate(-5.11px, -2.05px) scale(1.052); opacity: 0.793; animation-timing-function: linear; }
    3.5% { transform: translate(-10.21px, -3.79px) scale(1.098); opacity: 0.609; animation-timing-function: linear; }
    5.25% { transform: translate(-14.75px, -5.21px) scale(1.139); opacity: 0.445; animation-timing-function: linear; }
    7% { transform: translate(-18.41px, -6.31px) scale(1.175); opacity: 0.301; animation-timing-function: linear; }
    8.75% { transform: translate(-21.07px, -7.09px) scale(1.205); opacity: 0.181; animation-timing-function: linear; }
    10.5% { transform: translate(-22.78px, -7.61px) scale(1.228); opacity: 0.086; animation-timing-function: linear; }
    12.25% { transform: translate(-23.71px, -7.9px) scale(1.244); opacity: 0.023; animation-timing-function: linear; }
    14% { transform: translate(-24px, -8px) scale(1.25); opacity: 0; animation-timing-function: linear; }
    14.5%, 16% { transform: translate(40px, 38px) scale(0.25); opacity: 0; animation-timing-function: linear; }
    19% { transform: translate(39.87px, 37.79px) scale(0.259); opacity: 0.333; animation-timing-function: linear; }
    22% { transform: translate(39.37px, 36.96px) scale(0.288); opacity: 0.667; animation-timing-function: linear; }
    25% { transform: translate(38.24px, 35.01px) scale(0.34); opacity: 1; animation-timing-function: linear; }
    28% { transform: translate(36.06px, 31.4px) scale(0.416); opacity: 1; animation-timing-function: linear; }
    31% { transform: translate(32.44px, 26.26px) scale(0.514); opacity: 1; animation-timing-function: linear; }
    34% { transform: translate(27.31px, 19.56px) scale(0.625); opacity: 1; animation-timing-function: linear; }
    37% { transform: translate(20.9px, 12.76px) scale(0.736); opacity: 1; animation-timing-function: linear; }
    40% { transform: translate(13.96px, 7.6px) scale(0.834); opacity: 1; animation-timing-function: linear; }
    43% { transform: translate(7.79px, 3.91px) scale(0.91); opacity: 1; animation-timing-function: linear; }
    46% { transform: translate(3.34px, 1.58px) scale(0.962); opacity: 1; animation-timing-function: linear; }
    49% { transform: translate(0.8px, 0.37px) scale(0.991); opacity: 1; animation-timing-function: linear; }
    52%, 100% { transform: translate(0px, 0px) scale(1); opacity: 1; }
}


/* -----------------------------------------------------------------------------
   R2 Semiconductor Supremacy (reads/static/reads/thumbs/) — signals come in
   from both sides of the board, top pair first: a pulse runs along each trace
   and its pin into the package, each pin pair lights as its pulse arrives,
   then the die's core powers up; the pins go dark again in the same order.
   Pulses are one short dash (pathLength="100", the dash length set in the
   SVG) carried from just before the via to just past the package edge.
   -------------------------------------------------------------------------- */
.th-chip-pulse-1 { --th-k: th-chip-pulse-1; }
.th-chip-pulse-2 { --th-k: th-chip-pulse-2; }
.th-chip-pulse-3 { --th-k: th-chip-pulse-3; }
.th-chip-pin-1   { --th-k: th-chip-pin-1; }
.th-chip-pin-2   { --th-k: th-chip-pin-2; }
.th-chip-pin-3   { --th-k: th-chip-pin-3; }
.th-chip-core    { --th-k: th-chip-core; transform-origin: 160px 100px; }

@keyframes th-chip-pulse-1 {
    0%    { stroke-dashoffset: 21.24; opacity: 1; animation-timing-function: cubic-bezier(0, 0, .55, 1); }
    18% { stroke-dashoffset: -100.5; opacity: 1; }
    18.5%, 100% { stroke-dashoffset: -100.5; opacity: 0; }
}
@keyframes th-chip-pulse-2 {
    0%, 6% { stroke-dashoffset: 24.18; opacity: 1; }
    24% { stroke-dashoffset: -100.5; opacity: 1; }
    24.5%, 100% { stroke-dashoffset: -100.5; opacity: 0; }
}
@keyframes th-chip-pulse-3 {
    0%, 12% { stroke-dashoffset: 21.24; opacity: 1; }
    30% { stroke-dashoffset: -100.5; opacity: 1; }
    30.5%, 100% { stroke-dashoffset: -100.5; opacity: 0; }
}
@keyframes th-chip-pin-1 {
    0%, 9.23% { opacity: 0; }
    11.23%, 58% { opacity: 1; }
    63%, 100% { opacity: 0; }
}
@keyframes th-chip-pin-2 {
    0%, 16.83% { opacity: 0; }
    18.83%, 61% { opacity: 1; }
    66%, 100% { opacity: 0; }
}
@keyframes th-chip-pin-3 {
    0%, 23.2% { opacity: 0; }
    25.2%, 64% { opacity: 1; }
    69%, 100% { opacity: 0; }
}
@keyframes th-chip-core {
    0%, 32% { transform: scale(.6); opacity: 0; }
    38%, 60% { transform: scale(1); opacity: 1; }
    68%, 100% { transform: scale(.6); opacity: 0; }
}
