/* ===========================================================================
   professions-effects.css — professions-effects.css — the profession effect layer, all bespoke.
   PASS 1 AMBIENCE (15) · PASS 2 MOMENTS (15) · PASS 3 PAGE SLOTS (60) · PASS 4 FEED (30)
   · PASS 5 ICONOGRAPHY (glyph particles, corner flourishes, border crawl, chips, badges)
   · PASS 6 FRAMES (a border ornament per profession + two new animations).

   Loaded after effects.css. One-way dependency, same as every cosmetic file:
   nothing in styles.css, skins.css or effects.css may reference anything here.

   ---------------------------------------------------------------------------
   BESPOKE ONLY. This file shares NO engine with effects.css.

   effects.css is 12 techniques pointed at 39 placements, and that was the right
   trade while the catalog was general — rain is particles falling through a
   layer, and the layer can be the window or a card. The professions are the
   point where that stops paying: fifteen elements that all read as "the shared
   particle engine, tinted" is fifteen professions that feel like one.

   So every engine below has its own @keyframes, its own particle geometry, and
   its own travel curve. No --fx-tint swap on an existing engine, no second
   unlock bolted onto a shipped effect, no `tech` value shared with an
   effects.css row. If a rule here could be replaced by adding a class to
   .fx-fall, it is in the wrong file.

   THE TRAVEL CURVE IS THE SIGNATURE. Not the colour — colour is the skin's job
   and a profession is already wearing one. What separates these is HOW a thing
   moves: an arc that stalls, a closed orbit, a reciprocating shuttle, a glint
   that never travels at all. Two professions may not share a curve.

   ---------------------------------------------------------------------------
   WHAT effects-run.js MUST DO

   Each engine is a `.fx-layer.fx-fixed` carrying its ENGINE CLASS, holding
   `.fx-p` children — the same host contract as every other particle effect, so
   the spawner does not change shape:

     <div class="fx-layer fx-fixed fx-pros-lantern">
       <span class="fx-p" style="--x:18%; --y:12%; --d:-2.4s; --sc:.8"></span>
       ...
     </div>

   Four custom properties per particle, all optional with sane fallbacks so an
   engine still reads if the spawner sets none:

     --x, --y   spawn position (percentages of the layer)
     --d       animation-delay, negative to start mid-cycle
     --sc      per-particle scale, ~0.6 to 1.4

   Particle COUNTS stay in effects-run.js so the intensity dial can thin them;
   this file only knows how one particle moves. Suggested counts are in the
   comment on each engine.

   Three engines need more than `.fx-p` and say so in their comment
   (tide-caustic needs none at all, weave-shuttle needs one `.fx-rail` child,
   beast-prowl pairs its particles).

   ---------------------------------------------------------------------------
   RULES CARRIED OVER, because they are structure rather than reuse

   · transform and opacity only on anything that runs forever. The one exception
     is forge-cinder's colour ramp, which is the effect's entire subject and runs
     on ~14 three-pixel elements.
   · never animate a property that affects document flow.
   · no fixed decoration with a negative inset — it adds scrollable area and
     broke the zero-overflow-at-390px guarantee once already. Every layer here
     is inset:0 and pans INSIDE its box.
   · band 0. Ambience is behind the whole app; .app-shell is band 1.
   · html[data-intensity="calm"] already hides .fx-layer wholesale in
     effects.css, and reduced-motion already stops it. Both inherited, nothing
     to restate.
   =========================================================================== */

/* ---------------------------------------------------------------------------
   Shared SPAWN POSITIONING only. This is not an engine — it is where a particle
   starts, which every engine needs and none of them differ on.
   --------------------------------------------------------------------------- */
.fx-layer[class*="fx-pros-"] > .fx-p,
.fx-layer[class*="fx-herb-"] > .fx-p,
.fx-layer[class*="fx-wood-"] > .fx-p,
.fx-layer[class*="fx-bone-"] > .fx-p,
.fx-layer[class*="fx-beast-"] > .fx-p,
.fx-layer[class*="fx-home-"] > .fx-p,
.fx-layer[class*="fx-api-"] > .fx-p,
.fx-layer[class*="fx-myco-"] > .fx-p,
.fx-layer[class*="fx-alch-"] > .fx-p,
.fx-layer[class*="fx-glass-"] > .fx-p,
.fx-layer[class*="fx-forge-"] > .fx-p,
.fx-layer[class*="fx-weave-"] > .fx-p,
.fx-layer[class*="fx-cook-"] > .fx-p,
.fx-layer[class*="fx-scribe-"] > .fx-p {
  left: var(--x, 50%);
  top: var(--y, 50%);
  animation-delay: var(--d, 0s);
  will-change: transform, opacity;
}

/* ===========================================================================
   1 · PROSPECTOR — Lantern Dust          html[data-on-pros-lantern]
   Grit hanging in a lamp beam. The signature is that the particle is BRIGHTEST
   IN THE MIDDLE THIRD of its fall and nearly invisible at either end — it is
   not fading in and out, it is crossing a beam. Barely falls at all; the sway
   is wider than the drop, which is what makes the air read as still.
   Geometry: a 2px square turned 45deg. Grit has corners; a circle reads as a
   bubble. Suggested count: 16.
   =========================================================================== */
html[data-on-pros-lantern] .fx-pros-lantern > .fx-p {
  width: 2px;
  height: 2px;
  background: rgba(255, 224, 168, 0.95);
  animation: pros-lantern 19s cubic-bezier(.44, .06, .58, .94) infinite;
}
@keyframes pros-lantern {
  0%   { transform: translate3d(0, -14vh, 0) rotate(45deg) scale(var(--sc, 1)); opacity: 0; }
  22%  { opacity: 0.14; }
  /* the beam */
  42%  { transform: translate3d(5.5vw, 12vh, 0) rotate(45deg) scale(var(--sc, 1)); opacity: 1; }
  58%  { transform: translate3d(2vw, 24vh, 0) rotate(45deg) scale(var(--sc, 1)); opacity: 1; }
  78%  { opacity: 0.12; }
  100% { transform: translate3d(-5vw, 48vh, 0) rotate(45deg) scale(var(--sc, 1)); opacity: 0; }
}
/* The beam itself, so the brightening has a cause. Static — a moving shaft
   reads as a searchlight. */
html[data-on-pros-lantern] .fx-pros-lantern::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(178deg,
    transparent 0 18%, rgba(255, 210, 122, 0.055) 34% 52%, transparent 74%);
}

/* ===========================================================================
   2 · HERBALIST — Seed Drift             html[data-on-herb-seeddrift]
   Wind-borne seed. The signature is a TRUE ARC: it rises, STALLS at the top of
   the arc with almost no vertical movement for a fifth of the cycle, then
   settles. Nothing else in the app has a curve that changes direction, and the
   stall is the whole reason it reads as caught rather than blown.
   Geometry: a seed head — a small disc with a soft pappus halo above it, drawn
   as two stacked radials on one element. Suggested count: 12.
   =========================================================================== */
html[data-on-herb-seeddrift] .fx-herb-seeddrift > .fx-p {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 50% 78%, rgba(158, 224, 138, 0.95) 0 16%, transparent 22%),
    radial-gradient(circle at 50% 40%, rgba(220, 240, 210, 0.32) 0 44%, transparent 66%);
  animation: herb-seeddrift 24s cubic-bezier(.3, .5, .35, 1) infinite;
}
@keyframes herb-seeddrift {
  0%   { transform: translate3d(0, 8vh, 0) rotate(0deg) scale(var(--sc, 1)); opacity: 0; }
  14%  { opacity: 0.9; }
  /* up */
  34%  { transform: translate3d(7vw, -16vh, 0) rotate(38deg) scale(var(--sc, 1)); }
  /* the stall — 20% of the cycle, 2vh of travel */
  44%  { transform: translate3d(11vw, -19vh, 0) rotate(52deg) scale(var(--sc, 1)); }
  58%  { transform: translate3d(15vw, -18vh, 0) rotate(41deg) scale(var(--sc, 1)); }
  /* settle */
  86%  { opacity: 0.85; }
  100% { transform: translate3d(24vw, 22vh, 0) rotate(96deg) scale(var(--sc, 1)); opacity: 0; }
}

/* ===========================================================================
   3 · WOODWRIGHT — Shavings              html[data-on-wood-shavings]
   Curls off a plane. The signature is the LANDING: a fast fall, then an
   over-rotation and a settle at the bottom of the window, where it rests
   visibly before fading. Everything else in the app fades out mid-flight; this
   one arrives somewhere.
   Geometry: an open curl — a ring with two corners squared off and no fill, so
   it reads as shaved rather than as a flake. Suggested count: 10.
   =========================================================================== */
html[data-on-wood-shavings] .fx-wood-shavings > .fx-p {
  width: 11px;
  height: 7px;
  border: 1.5px solid rgba(201, 138, 69, 0.8);
  border-left-color: transparent;
  border-radius: 60% 20% 60% 20%;
  animation: wood-shavings 13s cubic-bezier(.5, .02, .72, .55) infinite;
}
@keyframes wood-shavings {
  0%   { transform: translate3d(0, -12vh, 0) rotate(0deg) scale(var(--sc, 1)); opacity: 0; }
  9%   { opacity: 0.85; }
  /* fall, tumbling */
  62%  { transform: translate3d(3vw, 62vh, 0) rotate(430deg) scale(var(--sc, 1)); opacity: 0.85; }
  /* land, over-rotate */
  74%  { transform: translate3d(4.5vw, 82vh, 0) rotate(505deg) scale(var(--sc, 1)); }
  /* settle back */
  82%  { transform: translate3d(4.2vw, 80.5vh, 0) rotate(492deg) scale(var(--sc, 1)); }
  /* rest */
  92%  { transform: translate3d(4.2vw, 80.5vh, 0) rotate(492deg) scale(var(--sc, 1)); opacity: 0.7; }
  100% { transform: translate3d(4.2vw, 80.5vh, 0) rotate(492deg) scale(var(--sc, 1)); opacity: 0; }
}

/* ===========================================================================
   4 · TIDECALLER — Caustic Net           html[data-on-tide-caustic]
   NO PARTICLES AT ALL — the only ambience engine in the app with none, and the
   reason it is bespoke rather than a wash. Two crossing rakes of light whose
   CELLS STRETCH AND CONTRACT against each other at different periods, which is
   what surface caustics actually do. A wash pans; this deforms.
   effects-run.js should mount the layer with ZERO .fx-p children.
   =========================================================================== */
html[data-on-tide-caustic] .fx-tide-caustic::before,
html[data-on-tide-caustic] .fx-tide-caustic::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-repeat: repeat;
}
html[data-on-tide-caustic] .fx-tide-caustic::before {
  background-image: repeating-linear-gradient(58deg,
    transparent 0 26px, rgba(127, 227, 224, 0.075) 26px 29px, transparent 29px 62px);
  background-size: 260px 260px;
  animation: tide-caustic-a 17s ease-in-out infinite alternate;
}
html[data-on-tide-caustic] .fx-tide-caustic::after {
  background-image: repeating-linear-gradient(-63deg,
    transparent 0 33px, rgba(63, 169, 216, 0.065) 33px 36px, transparent 36px 74px);
  background-size: 300px 300px;
  animation: tide-caustic-b 23s ease-in-out infinite alternate;
}
/* background-size, not transform: the point is that the CELLS change shape.
   Two elements, one property each, no layout — the one place in this file where
   a non-composited property is the correct answer rather than a shortcut. */
@keyframes tide-caustic-a {
  0%   { background-size: 260px 260px; background-position: 0 0; }
  100% { background-size: 320px 224px; background-position: 40px 90px; }
}
@keyframes tide-caustic-b {
  0%   { background-size: 300px 300px; background-position: 0 0; }
  100% { background-size: 248px 356px; background-position: -60px 70px; }
}

/* ===========================================================================
   5 · BONE-HUNTER — Ashfall              html[data-on-bone-ashfall]
   Ash. The signature is that it is DEAD STRAIGHT and STUTTERS: steps() timing,
   no sway, no rotation, no easing. Every other falling thing in the app drifts,
   because drift reads as alive. This one refuses to, and that refusal is the
   effect.
   Geometry: a 1px fleck, irregular via border-radius on one corner only, at the
   lowest opacity of any engine here. Suggested count: 22 (they are tiny).
   =========================================================================== */
html[data-on-bone-ashfall] .fx-bone-ashfall > .fx-p {
  width: 1.5px;
  height: 1.5px;
  border-radius: 0 40% 0 0;
  background: rgba(206, 206, 216, 0.7);
  animation: bone-ashfall 21s steps(26, end) infinite;
}
@keyframes bone-ashfall {
  0%   { transform: translate3d(0, -8vh, 0) scale(var(--sc, 1)); opacity: 0; }
  8%   { opacity: 0.55; }
  84%  { opacity: 0.4; }
  100% { transform: translate3d(0, 104vh, 0) scale(var(--sc, 1)); opacity: 0; }
}
/* A cold floor for it to fall onto. No warm light anywhere in this element. */
html[data-on-bone-ashfall] .fx-bone-ashfall::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(120% 40% at 50% 104%, rgba(206, 206, 216, 0.05), transparent 66%);
}

/* ===========================================================================
   6 · BEASTMASTER — Prowl                html[data-on-beast-prowl]
   Something crossing in the dark. The signature is HORIZONTAL TRAVEL — the only
   ambience engine that does not move vertically — and that it BLINKS OUT
   mid-crossing and returns further along, so you lose it and find it again.
   Geometry: a pair. Each .fx-p is one eye and draws its partner as a
   pseudo-element at a fixed offset, so the two can never drift apart.
   Suggested count: 3. More than three and it is a motorway.
   =========================================================================== */
html[data-on-beast-prowl] .fx-beast-prowl > .fx-p {
  width: 7px;
  height: 4px;
  border-radius: 50%;
  background: rgba(255, 176, 112, 0.9);
  box-shadow: 0 0 9px 2px rgba(255, 164, 100, 0.45);
  animation: beast-prowl 27s linear infinite;
}
html[data-on-beast-prowl] .fx-beast-prowl > .fx-p::after {
  content: "";
  position: absolute;
  left: 15px;
  top: 1px;
  width: 6px;
  height: 3.5px;
  border-radius: 50%;
  background: rgba(255, 176, 112, 0.75);
  box-shadow: 0 0 8px 2px rgba(255, 164, 100, 0.35);
}
@keyframes beast-prowl {
  0%   { transform: translate3d(-14vw, 0, 0) scale(var(--sc, 1)); opacity: 0; }
  7%   { opacity: 0.9; }
  /* lost */
  28%  { opacity: 0.9; }
  31%  { opacity: 0; }
  46%  { transform: translate3d(38vw, 1.5vh, 0) scale(var(--sc, 1)); opacity: 0; }
  /* found, further along and lower */
  50%  { opacity: 0.95; }
  76%  { opacity: 0.9; }
  79%  { opacity: 0; }
  100% { transform: translate3d(112vw, 3vh, 0) scale(var(--sc, 1)); opacity: 0; }
}

/* ===========================================================================
   7 · HOMESTEADER — Chaff                html[data-on-home-chaff]
   Chaff off a threshing floor. The signature is GUSTS: the particle travels in
   two hard pushes with a near-stationary hover between them, so a field of them
   surges rather than streams. Predominantly sideways, barely rising.
   Geometry: a 6x1px sliver — the thinnest particle in the app, because chaff is
   husk, not seed. Suggested count: 18, spawned in clusters of 5-6 sharing a
   delay so the gusts read as one wind.
   =========================================================================== */
html[data-on-home-chaff] .fx-home-chaff > .fx-p {
  width: 6px;
  height: 1px;
  border-radius: 1px;
  background: rgba(226, 214, 168, 0.8);
  animation: home-chaff 16s cubic-bezier(.16, .82, .3, .98) infinite;
}
@keyframes home-chaff {
  0%   { transform: translate3d(-8vw, 0, 0) rotate(-6deg) scale(var(--sc, 1)); opacity: 0; }
  10%  { opacity: 0.8; }
  /* gust */
  26%  { transform: translate3d(26vw, -3vh, 0) rotate(14deg) scale(var(--sc, 1)); }
  /* hover */
  44%  { transform: translate3d(31vw, -1.5vh, 0) rotate(-3deg) scale(var(--sc, 1)); }
  /* gust */
  72%  { transform: translate3d(74vw, -5vh, 0) rotate(19deg) scale(var(--sc, 1)); }
  88%  { opacity: 0.75; }
  100% { transform: translate3d(108vw, -2vh, 0) rotate(8deg) scale(var(--sc, 1)); opacity: 0; }
}

/* ===========================================================================
   8 · APIARIST — Swarm                   html[data-on-api-swarm]
   The only CLOSED-PATH engine in the app: each particle traces its own small
   ellipse and returns, so nothing ever enters or leaves the window. That is the
   hive read — one body, many wings, going nowhere.
   Built as a rotating arm with the bee pinned to its end, which is rounder and
   far cheaper than animating an offset-path. The arm is the .fx-p; the bee is
   its pseudo-element.
   Geometry: a 3px body with a pale wing blur behind it. Suggested count: 14.
   =========================================================================== */
html[data-on-api-swarm] .fx-api-swarm > .fx-p {
  width: calc(var(--sc, 1) * 46px);
  height: calc(var(--sc, 1) * 30px);
  margin: calc(var(--sc, 1) * -15px) 0 0 calc(var(--sc, 1) * -23px);
  animation: api-swarm-orbit calc(3.4s * var(--sc, 1)) linear infinite;
}
html[data-on-api-swarm] .fx-api-swarm > .fx-p::after {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: 3.5px;
  height: 3px;
  margin-left: -1.75px;
  border-radius: 50%;
  background: rgba(255, 194, 122, 0.95);
  box-shadow: 0 0 5px 1.5px rgba(242, 115, 155, 0.35);
  /* the wings — a second, faster loop on the bee itself, so it jitters along
     the orbit instead of gliding round it */
  animation: api-swarm-wing 0.16s steps(2, end) infinite;
}
@keyframes api-swarm-orbit {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
@keyframes api-swarm-wing {
  0%   { transform: translate3d(0, 0, 0); }
  100% { transform: translate3d(0.5px, -0.5px, 0); }
}

/* ===========================================================================
   9 · MYCOLOGIST — Spore Puff            html[data-on-myco-spore]
   Two phases, which no other engine has: a spore is RELEASED — thrown outward
   from a point, fast, in the first eighth of the cycle — and then DRIFTS DOWN
   for the remaining seven eighths. The velocity discontinuity is the effect.
   The outward direction comes from --a (an angle) so a cluster sharing a spawn
   point reads as one cap firing.
   Geometry: a 2.5px disc that GROWS as it drifts, like something taking up
   water. Suggested count: 20, in clusters of 6-8 sharing --x/--y and --d.
   =========================================================================== */
html[data-on-myco-spore] .fx-myco-spore > .fx-p {
  width: 2.5px;
  height: 2.5px;
  border-radius: 50%;
  background: rgba(255, 150, 132, 0.85);
  box-shadow: 0 0 4px 1px rgba(192, 132, 232, 0.3);
  animation: myco-spore 29s cubic-bezier(.08, .78, .48, 1) infinite;
}
@keyframes myco-spore {
  /* release */
  0%   { transform: translate3d(0, 0, 0) scale(0.3); opacity: 0; }
  3%   { opacity: 0.95; }
  12%  { transform:
           translate3d(calc(cos(var(--a, 40deg)) * 9vw), calc(sin(var(--a, 40deg)) * -7vh), 0)
           scale(calc(var(--sc, 1) * 0.8)); }
  /* drift */
  100% { transform:
           translate3d(calc(cos(var(--a, 40deg)) * 13vw), 46vh, 0)
           scale(calc(var(--sc, 1) * 1.5)); opacity: 0; }
}
/* Fallback for engines without trig support: a plain outward-right release.
   The effect degrades to one direction rather than to nothing. */
@supports not (width: calc(cos(45deg) * 1px)) {
  @keyframes myco-spore {
    0%   { transform: translate3d(0, 0, 0) scale(0.3); opacity: 0; }
    3%   { opacity: 0.95; }
    12%  { transform: translate3d(7vw, -6vh, 0) scale(calc(var(--sc, 1) * 0.8)); }
    100% { transform: translate3d(11vw, 46vh, 0) scale(calc(var(--sc, 1) * 1.5)); opacity: 0; }
  }
}

/* ===========================================================================
   10 · ALCHEMIST — Vapour                html[data-on-alch-vapour]
   A plume hitting a ceiling. The signature is the SHEAR: the particle rises as
   a narrow column, then at the top it stops rising and spreads sideways while
   flattening — scaleX climbs as scaleY collapses. Nothing else here deforms.
   Geometry: a soft vertical ellipse, no hard edge — vapour has no outline.
   Suggested count: 9. It is a large particle.
   =========================================================================== */
html[data-on-alch-vapour] .fx-alch-vapour > .fx-p {
  width: 26px;
  height: 44px;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(123, 139, 245, 0.22), transparent 74%);
  animation: alch-vapour 15s cubic-bezier(.3, .1, .5, .9) infinite;
}
@keyframes alch-vapour {
  0%   { transform: translate3d(0, 12vh, 0) scale(0.4, 0.5); opacity: 0; }
  12%  { opacity: 0.9; }
  /* the column */
  56%  { transform: translate3d(1.5vw, -22vh, 0) scale(calc(var(--sc, 1) * 0.85), calc(var(--sc, 1) * 1.25)); opacity: 0.85; }
  /* the shear */
  100% { transform: translate3d(4vw, -34vh, 0) scale(calc(var(--sc, 1) * 3.1), calc(var(--sc, 1) * 0.28)); opacity: 0; }
}

/* ===========================================================================
   11 · GLASSWORKER — Glints              html[data-on-glass-glint]
   ZERO TRAVEL. The particle never moves: it flares in place and vanishes, and
   another flares somewhere else. Light catching a facet does not drift, and
   every other engine in this file is about motion — this one is about its
   absence, which is why it cannot be any of them with a class added.
   Geometry: a four-point star drawn as two crossed bars on one element, which
   is the shape a glint reads as at 6px. Suggested count: 12, well spread; the
   spawner should re-randomise --x/--y on animationiteration if it can, and the
   effect still works if it cannot.
   =========================================================================== */
html[data-on-glass-glint] .fx-glass-glint > .fx-p {
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  background:
    linear-gradient(0deg, transparent 42%, rgba(230, 250, 255, 0.95) 49% 51%, transparent 58%),
    linear-gradient(90deg, transparent 42%, rgba(230, 250, 255, 0.95) 49% 51%, transparent 58%);
  animation: glass-glint 6.5s cubic-bezier(.1, .9, .2, 1) infinite;
}
@keyframes glass-glint {
  0%,  62% { transform: scale(0) rotate(0deg); opacity: 0; }
  70%      { transform: scale(calc(var(--sc, 1) * 1.15)) rotate(24deg); opacity: 1; }
  76%      { transform: scale(calc(var(--sc, 1) * 0.7)) rotate(34deg); opacity: 0.55; }
  82%      { transform: scale(calc(var(--sc, 1) * 0.95)) rotate(42deg); opacity: 0.85; }
  100%     { transform: scale(0) rotate(58deg); opacity: 0; }
}

/* ===========================================================================
   12 · FORGEWRIGHT — Cinders             html[data-on-forge-cinder]
   Rising cinders that COOL AS THEY CLIMB — white-hot at the anvil, orange at
   head height, dead red at the roof — and DECELERATE while they do it. The
   colour ramp is the subject, not decoration, which is the one place in this
   file where animating a paint property is the correct answer. It runs on
   fourteen three-pixel elements.
   The flicker is a second, much faster animation on the same element's
   pseudo-child, so the two do not have to share a duration.
   Suggested count: 14.
   =========================================================================== */
html[data-on-forge-cinder] .fx-forge-cinder > .fx-p {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  animation: forge-cinder 8.5s cubic-bezier(.12, .62, .3, 1) infinite;
}
@keyframes forge-cinder {
  0%   { transform: translate3d(0, 10vh, 0) scale(calc(var(--sc, 1) * 1.15));
         background: #fff6e0; box-shadow: 0 0 7px 2px rgba(255, 240, 200, 0.7); opacity: 0; }
  8%   { opacity: 1; }
  /* orange, and already slowing */
  46%  { transform: translate3d(2.5vw, -20vh, 0) scale(calc(var(--sc, 1) * 0.85));
         background: #ff8a3c; box-shadow: 0 0 6px 1.5px rgba(255, 138, 60, 0.5); opacity: 1; }
  /* dead red, barely moving */
  100% { transform: translate3d(-1vw, -44vh, 0) scale(calc(var(--sc, 1) * 0.35));
         background: #a32718; box-shadow: 0 0 3px 1px rgba(163, 39, 24, 0.3); opacity: 0; }
}
/* The draught. A cinder does not rise in a straight line and the wobble must
   not share the cooling curve's duration, so it lives on a child. */
html[data-on-forge-cinder] .fx-forge-cinder > .fx-p::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  animation: forge-cinder-draught 1.3s ease-in-out infinite alternate;
}
@keyframes forge-cinder-draught {
  from { transform: translate3d(-2.5px, 0, 0); }
  to   { transform: translate3d(2.5px, 0, 0); }
}

/* ===========================================================================
   13 · WEAVER — Shuttle                  html[data-on-weave-shuttle]
   RECIPROCATING, which nothing else in the app is: the shuttle crosses left to
   right, pauses at the selvedge, and comes back. Every other engine travels one
   way and respawns.
   Two parts, and effects-run.js must mount the second: one `.fx-rail` child for
   the cloth building up behind the shuttle, plus the .fx-p shuttles themselves.
   Suggested count: 3 shuttles at different heights and speeds, 1 rail.
   =========================================================================== */
html[data-on-weave-shuttle] .fx-weave-shuttle > .fx-p {
  width: 16px;
  height: 3px;
  border-radius: 3px 60% 3px 60%;
  background: linear-gradient(90deg, transparent, rgba(245, 143, 212, 0.9) 40%, rgba(245, 143, 212, 0.55));
  animation: weave-shuttle 21s cubic-bezier(.42, 0, .58, 1) infinite;
}
@keyframes weave-shuttle {
  0%   { transform: translate3d(-6vw, 0, 0) scaleX(1); opacity: 0; }
  6%   { opacity: 0.9; }
  /* across */
  40%  { transform: translate3d(102vw, 0, 0) scaleX(1); opacity: 0.9; }
  /* the selvedge — it stops, and turns over */
  46%  { transform: translate3d(104vw, 0, 0) scaleX(1); }
  50%  { transform: translate3d(104vw, 0, 0) scaleX(-1); }
  /* back */
  90%  { transform: translate3d(-4vw, 0, 0) scaleX(-1); opacity: 0.9; }
  96%  { transform: translate3d(-6vw, 0, 0) scaleX(-1); opacity: 0; }
  100% { transform: translate3d(-6vw, 0, 0) scaleX(1); opacity: 0; }
}
/* The cloth. Rows accumulate from the bottom in step with the shuttle's
   crossings — background-size on one element, no per-row nodes. */
html[data-on-weave-shuttle] .fx-weave-shuttle > .fx-rail {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: repeating-linear-gradient(0deg,
    rgba(226, 99, 189, 0.075) 0 2px, transparent 2px 9px);
  background-repeat: repeat-x;
  background-position: 0 100%;
  animation: weave-shuttle-cloth 21s steps(9, end) infinite;
}
@keyframes weave-shuttle-cloth {
  0%   { background-size: 100% 0; }
  100% { background-size: 100% 62%; }
}

/* ===========================================================================
   14 · COOK — Steam                      html[data-on-cook-steam]
   A broadening spiral. The signature is that the particle ROTATES ABOUT ITS OWN
   CENTRE while rising and swelling, so the plume curls — where the Alchemist's
   vapour shears flat against a ceiling, this one turns.
   Geometry: a crescent, so the rotation is visible. A disc rotating looks
   static, which is the mistake this replaces.
   Suggested count: 11.
   =========================================================================== */
html[data-on-cook-steam] .fx-cook-steam > .fx-p {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 4px solid rgba(240, 236, 214, 0.2);
  border-right-color: transparent;
  border-bottom-color: transparent;
  animation: cook-steam 12s cubic-bezier(.34, .06, .5, .96) infinite;
}
@keyframes cook-steam {
  0%   { transform: translate3d(0, 14vh, 0) rotate(0deg) scale(0.35); opacity: 0; }
  16%  { opacity: 0.9; }
  50%  { transform: translate3d(3vw, -6vh, 0) rotate(210deg) scale(calc(var(--sc, 1) * 0.95)); opacity: 0.7; }
  100% { transform: translate3d(-2vw, -30vh, 0) rotate(430deg) scale(calc(var(--sc, 1) * 2.2)); opacity: 0; }
}

/* ===========================================================================
   15 · SCRIBE — Lamplight Motes          html[data-on-scribe-motes]
   The quietest engine in the app, deliberately. Dust in a reading lamp: the
   particle travels perhaps a tenth of the window over ninety seconds, and the
   only thing you actually notice is that it BLINKS as it turns — a mote catching
   the lamp and losing it. Nothing arrives, nothing leaves.
   The blink is on a separate, prime-ish duration so it never lands in step with
   the drift; that non-resolution is what stops it reading as a pulse.
   Geometry: a 1.5px point, no glow. Suggested count: 20.
   =========================================================================== */
html[data-on-scribe-motes] .fx-scribe-motes > .fx-p {
  width: 1.5px;
  height: 1.5px;
  border-radius: 50%;
  background: rgba(240, 232, 210, 0.9);
  animation:
    scribe-motes-drift 92s ease-in-out infinite alternate,
    scribe-motes-turn 7.3s ease-in-out infinite;
}
@keyframes scribe-motes-drift {
  from { transform: translate3d(-2vw, 1vh, 0) scale(var(--sc, 1)); }
  to   { transform: translate3d(3vw, -2vh, 0) scale(var(--sc, 1)); }
}
@keyframes scribe-motes-turn {
  0%, 100% { opacity: 0.1; }
  46%      { opacity: 0.85; }
}
/* The lamp. Warm, off-centre, and still — the reason the motes have anything to
   catch. Off-centre because a centred lamp reads as a vignette. */
html[data-on-scribe-motes] .fx-scribe-motes::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(46% 40% at 22% 14%, rgba(221, 196, 140, 0.06), transparent 70%);
}


/* ===========================================================================
   PASS 2 of 4: MOMENTS — the one-shot, fired on a level-up or a completion.

   Fifteen engines, no shared machinery. effects.css already has two one-shots
   (.fx-flash-once and .fx-draw-overlay) and neither is reused here: a flash is a
   flash whoever earns it, which is exactly the sameness the directive retires.
   These are fifteen different EVENTS — something cracks, something unfurls,
   something falls, something is counted, something is assembled.

   ---------------------------------------------------------------------------
   HOW ONE FIRES

   effects-run.js owns the runtime: its MOMENT_ENGINES table holds, per engine,
   the child markup each "Markup:" header below states and the duration each
   header states. FX.moment('level.up') mounts the worn engine's overlay
   (div.fx-moment.fx-moment-<id>), lets it play, and removes it on a timeout of
   that duration — NOT on animationend, because a multi-child engine's children
   end at different times. If you change an engine's markup or duration here,
   change its table row there in the same edit; a suite assertion holds the two
   files together.

   Band 940, from the layering table: deliberately ABOVE the top bar so a
   level-up covers everything, and BELOW modals so it never obscures a dialog
   you have to answer. Do not invent a number.

   EVERY animation is `forwards` and finite. Nothing here may loop — a moment
   that is still running a minute later is furniture, and furniture belongs in
   the ambience slots.

   Durations are stated per engine and are the value `Moments.duration()` must
   return. They run 1.1s to 2.6s; past about 2.6s a celebration becomes a wait.
   =========================================================================== */

.fx-moment {
  position: fixed;
  inset: 0;
  z-index: 940;
  pointer-events: none;
  overflow: hidden;
}
.fx-moment > * { position: absolute; }
/* The centre, which most of these work outward from. Declared once because it
   is a coordinate, not an engine. */
.fx-moment .m-c { left: 50%; top: 50%; }

@media (prefers-reduced-motion: reduce) { .fx-moment { display: none !important; } }
html[data-intensity="calm"] .fx-moment { display: none !important; }

/* ===========================================================================
   1 · PROSPECTOR — Strike               .fx-moment-pros-strike        1.6s
   The pick lands and the stone SPLITS. Four fracture wedges drive outward from
   the point of impact on different timings — a fracture propagates unevenly, and
   four wedges arriving together would read as a star. The gem behind them flares
   only once the last crack has run.
   Markup: <i class="m-c m-frac f1"></i> x4 (f1..f4), <i class="m-c m-gem"></i>
   =========================================================================== */
.fx-moment-pros-strike .m-frac {
  width: 2px;
  height: 2px;
  background: rgba(255, 232, 180, 0.95);
  transform-origin: 0 50%;
  animation: pros-strike-crack 0.6s cubic-bezier(.05, .8, .3, 1) forwards;
}
.fx-moment-pros-strike .f1 { transform: rotate(-24deg); }
.fx-moment-pros-strike .f2 { transform: rotate(61deg);  animation-delay: 0.07s; }
.fx-moment-pros-strike .f3 { transform: rotate(154deg); animation-delay: 0.13s; }
.fx-moment-pros-strike .f4 { transform: rotate(233deg); animation-delay: 0.04s; }
@keyframes pros-strike-crack {
  0%   { width: 2px; opacity: 0; }
  10%  { opacity: 1; }
  100% { width: min(34vw, 320px); opacity: 0.85; }
}
.fx-moment-pros-strike .m-gem {
  width: min(20vw, 190px);
  height: min(20vw, 190px);
  margin: min(-10vw, -95px) 0 0 min(-10vw, -95px);
  border-radius: 50%;
  background: conic-gradient(from 18deg,
    rgba(255, 210, 122, 0.55) 0deg 34deg, transparent 34deg 62deg,
    rgba(255, 232, 180, 0.45) 62deg 78deg, transparent 78deg 140deg,
    rgba(232, 161, 60, 0.5) 140deg 186deg, transparent 186deg 360deg);
  animation: pros-strike-gem 0.95s cubic-bezier(.2, .7, .3, 1) 0.55s forwards;
}
@keyframes pros-strike-gem {
  0%   { transform: scale(0.2) rotate(0deg); opacity: 0; }
  30%  { transform: scale(1.06) rotate(26deg); opacity: 1; }
  100% { transform: scale(1.5) rotate(52deg); opacity: 0; }
}

/* ===========================================================================
   2 · HERBALIST — Unfurl                .fx-moment-herb-unfurl        2.2s
   A frond opens. One element, and the whole effect is in the transform order:
   it UNROLLS — rotating open from a tight coil while lengthening — so the tip
   travels much further than the base. No particles, nothing bursts.
   Markup: <i class="m-frond"></i>, <i class="m-leaf l1"></i> x5 (l1..l5)
   =========================================================================== */
.fx-moment-herb-unfurl .m-frond {
  left: 50%;
  bottom: -4vh;
  width: 3px;
  height: min(46vh, 420px);
  margin-left: -1.5px;
  border-radius: 3px;
  background: linear-gradient(0deg, rgba(116, 207, 138, 0.9), rgba(158, 224, 138, 0.25));
  transform-origin: 50% 100%;
  animation: herb-unfurl-stem 1.5s cubic-bezier(.16, .84, .28, 1) forwards;
}
@keyframes herb-unfurl-stem {
  0%   { transform: rotate(-38deg) scaleY(0.04); opacity: 0; }
  14%  { opacity: 0.95; }
  100% { transform: rotate(4deg) scaleY(1); opacity: 0.95; }
}
.fx-moment-herb-unfurl .m-leaf {
  left: 50%;
  bottom: -4vh;
  width: min(13vw, 120px);
  height: 10px;
  border-radius: 0 100% 0 100%;
  background: rgba(116, 207, 138, 0.55);
  transform-origin: 0 50%;
  animation: herb-unfurl-leaf 0.85s cubic-bezier(.2, .8, .3, 1) forwards;
}
/* Each leaf opens as the stem passes its height, alternating sides — which is
   what makes the frond read as growing rather than as being drawn. */
.fx-moment-herb-unfurl .l1 { bottom: 8vh;  transform: rotate(20deg);   animation-delay: 0.34s; }
.fx-moment-herb-unfurl .l2 { bottom: 17vh; transform: rotate(-200deg); animation-delay: 0.52s; }
.fx-moment-herb-unfurl .l3 { bottom: 26vh; transform: rotate(14deg);   animation-delay: 0.7s; }
.fx-moment-herb-unfurl .l4 { bottom: 34vh; transform: rotate(-194deg); animation-delay: 0.88s; }
.fx-moment-herb-unfurl .l5 { bottom: 41vh; transform: rotate(8deg);    animation-delay: 1.06s; }
@keyframes herb-unfurl-leaf {
  0%   { opacity: 0; width: 4px; }
  20%  { opacity: 0.7; }
  70%  { opacity: 0.7; }
  100% { opacity: 0; width: min(13vw, 120px); }
}

/* ===========================================================================
   3 · WOODWRIGHT — Felled               .fx-moment-wood-fell          1.9s
   A tree comes down. A single tall bar rotates about its BASE from vertical
   through to horizontal, decelerating hard at the end, and the dust goes up at
   the pivot rather than at the crown — which is the detail that makes it read
   as weight.
   Markup: <i class="m-trunk"></i>, <i class="m-dust"></i>
   =========================================================================== */
.fx-moment-wood-fell .m-trunk {
  left: 34%;
  bottom: 6vh;
  width: min(2.4vw, 22px);
  height: min(56vh, 500px);
  border-radius: 3px;
  background: linear-gradient(90deg, rgba(120, 78, 38, 0.9), rgba(201, 138, 69, 0.9) 40%, rgba(120, 78, 38, 0.85));
  transform-origin: 50% 100%;
  animation: wood-fell-trunk 1.55s cubic-bezier(.5, .02, .68, 1) forwards;
}
@keyframes wood-fell-trunk {
  0%   { transform: rotate(0deg); opacity: 0; }
  8%   { transform: rotate(-2deg); opacity: 0.95; }
  /* the go */
  30%  { transform: rotate(9deg); }
  /* down */
  84%  { transform: rotate(88deg); }
  /* the bounce, small — timber does not bounce much */
  91%  { transform: rotate(84deg); }
  100% { transform: rotate(87deg); opacity: 0; }
}
.fx-moment-wood-fell .m-dust {
  left: 34%;
  bottom: 4vh;
  width: min(26vw, 240px);
  height: min(12vh, 110px);
  margin-left: min(-13vw, -120px);
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(201, 138, 69, 0.3), transparent 72%);
  animation: wood-fell-dust 0.85s cubic-bezier(.1, .7, .3, 1) 1.05s forwards;
}
@keyframes wood-fell-dust {
  0%   { transform: scale(0.2, 0.3); opacity: 0; }
  25%  { opacity: 0.9; }
  100% { transform: scale(1.6, 1.9); opacity: 0; }
}

/* ===========================================================================
   4 · TIDECALLER — Surge                .fx-moment-tide-surge         2.4s
   A crest crosses the whole window bottom to top and BREAKS — the wave face
   rises as one mass, and at the top the foam detaches and keeps going while the
   face falls back. Two elements travelling at different speeds is the break.
   Markup: <i class="m-face"></i>, <i class="m-foam"></i>
   =========================================================================== */
.fx-moment-tide-surge .m-face {
  left: -10%;
  right: -10%;
  bottom: 0;
  height: 130vh;
  border-radius: 46% 54% 0 0 / 18% 22% 0 0;
  background: linear-gradient(0deg, rgba(30, 95, 143, 0.5), rgba(63, 169, 216, 0.3) 40%, transparent 72%);
  animation: tide-surge-face 2.05s cubic-bezier(.3, .1, .4, 1) forwards;
}
@keyframes tide-surge-face {
  0%   { transform: translate3d(0, 108%, 0) scaleY(0.7); opacity: 0; }
  16%  { opacity: 1; }
  /* the crest */
  58%  { transform: translate3d(0, -6%, 0) scaleY(1); }
  /* falls back */
  100% { transform: translate3d(0, 62%, 0) scaleY(0.85); opacity: 0; }
}
.fx-moment-tide-surge .m-foam {
  left: -10%;
  right: -10%;
  bottom: 0;
  height: 22vh;
  background:
    radial-gradient(closest-side at 18% 60%, rgba(215, 248, 252, 0.55), transparent 70%),
    radial-gradient(closest-side at 52% 40%, rgba(215, 248, 252, 0.45), transparent 70%),
    radial-gradient(closest-side at 84% 66%, rgba(215, 248, 252, 0.5), transparent 70%);
  animation: tide-surge-foam 1.75s cubic-bezier(.18, .74, .3, 1) 0.5s forwards;
}
/* Detaches — the foam keeps rising after the face has turned. */
@keyframes tide-surge-foam {
  0%   { transform: translate3d(0, 40vh, 0) scaleY(0.6); opacity: 0; }
  22%  { opacity: 0.95; }
  100% { transform: translate3d(0, -78vh, 0) scaleY(1.5); opacity: 0; }
}

/* ===========================================================================
   5 · BONE-HUNTER — Tally               .fx-moment-bone-tally         2.0s
   Marks CUT one at a time, counted. Five strokes appear in strict sequence with
   a hard step on each, hold together for a beat, then go at once. The only
   moment in the app that is discrete rather than continuous, and the only one
   that ends by simply stopping.
   Markup: <i class="m-cut c1"></i> x5 (c1..c5)
   =========================================================================== */
.fx-moment-bone-tally .m-cut {
  top: 50%;
  width: 3px;
  height: min(16vh, 140px);
  margin-top: min(-8vh, -70px);
  background: rgba(226, 226, 234, 0.92);
  transform-origin: 50% 0;
  animation: bone-tally-cut 1.6s steps(1, end) forwards;
}
.fx-moment-bone-tally .c1 { left: calc(50% - 68px); animation-delay: 0.05s; }
.fx-moment-bone-tally .c2 { left: calc(50% - 34px); animation-delay: 0.27s; }
.fx-moment-bone-tally .c3 { left: 50%;              animation-delay: 0.49s; }
.fx-moment-bone-tally .c4 { left: calc(50% + 34px); animation-delay: 0.71s; }
/* The fifth is struck THROUGH the other four, which is how a tally of five is
   actually written. */
.fx-moment-bone-tally .c5 {
  left: calc(50% - 78px);
  top: 50%;
  width: min(24vw, 190px);
  height: 3px;
  margin-top: -1.5px;
  transform: rotate(-24deg);
  transform-origin: 0 50%;
  animation-delay: 0.95s;
}
@keyframes bone-tally-cut {
  0%   { opacity: 0; }
  50%  { opacity: 1; }
  100% { opacity: 0; }
}

/* ===========================================================================
   6 · BEASTMASTER — Rake                .fx-moment-beast-rake         1.4s
   Four claw marks TEAR ACROSS the window on a diagonal, arriving as a group but
   not quite together — the outer two land a frame late, which is what stops it
   reading as a wipe. The fastest moment here; a swipe that takes two seconds is
   not a swipe.
   Markup: <i class="m-rake r1"></i> x4, <i class="m-c m-flash"></i>
   =========================================================================== */
.fx-moment-beast-rake .m-rake {
  top: -20vh;
  left: -10vw;
  width: min(1vw, 9px);
  height: 150vh;
  border-radius: 9px;
  background: linear-gradient(0deg, transparent, rgba(211, 170, 255, 0.85) 22%, rgba(255, 176, 112, 0.9) 60%, transparent);
  transform: rotate(24deg);
  animation: beast-rake-mark 0.72s cubic-bezier(.2, .86, .3, 1) forwards;
}
.fx-moment-beast-rake .r1 { margin-left: 0;    animation-delay: 0.06s; }
.fx-moment-beast-rake .r2 { margin-left: 26px; animation-delay: 0s; }
.fx-moment-beast-rake .r3 { margin-left: 52px; animation-delay: 0.02s; }
.fx-moment-beast-rake .r4 { margin-left: 78px; animation-delay: 0.08s; }
@keyframes beast-rake-mark {
  0%   { transform: rotate(24deg) translate3d(-30vw, 0, 0); opacity: 0; }
  18%  { opacity: 1; }
  100% { transform: rotate(24deg) translate3d(128vw, 0, 0); opacity: 0; }
}
.fx-moment-beast-rake .m-flash {
  width: 200vw;
  height: 200vh;
  margin: -100vh 0 0 -100vw;
  background: radial-gradient(closest-side, rgba(176, 122, 240, 0.22), transparent 62%);
  animation: beast-rake-flash 0.5s ease-out 0.1s forwards;
}
@keyframes beast-rake-flash {
  0%   { opacity: 0.9; }
  100% { opacity: 0; }
}

/* ===========================================================================
   7 · HOMESTEADER — Harvest             .fx-moment-home-harvest       2.3s
   The crop comes in. A band rises from the bottom edge, OVERSHOOTS, and settles
   back — the only moment that ends by coming to rest rather than by leaving, and
   the only one whose subject is arrival rather than release. Domestic on
   purpose: this element's reward is a full store, not a spectacle.
   Markup: <i class="m-sheaf"></i>, <i class="m-line"></i>
   =========================================================================== */
.fx-moment-home-harvest .m-sheaf {
  left: 0;
  right: 0;
  bottom: 0;
  height: 46vh;
  background:
    repeating-linear-gradient(93deg, rgba(226, 214, 168, 0.35) 0 2px, transparent 2px 13px),
    linear-gradient(0deg, rgba(82, 189, 176, 0.3), transparent 82%);
  animation: home-harvest-rise 1.9s cubic-bezier(.22, .9, .3, 1) forwards;
}
@keyframes home-harvest-rise {
  0%   { transform: translate3d(0, 100%, 0); opacity: 0; }
  12%  { opacity: 1; }
  /* overshoot */
  56%  { transform: translate3d(0, -9%, 0); }
  /* settle */
  74%  { transform: translate3d(0, 3%, 0); }
  86%  { transform: translate3d(0, 0, 0); opacity: 1; }
  100% { transform: translate3d(0, 0, 0); opacity: 0; }
}
.fx-moment-home-harvest .m-line {
  left: 0;
  right: 0;
  bottom: 46vh;
  height: 2px;
  background: linear-gradient(90deg, transparent, rgba(226, 214, 168, 0.7), transparent);
  animation: home-harvest-line 1.9s cubic-bezier(.22, .9, .3, 1) forwards;
}
@keyframes home-harvest-line {
  0%   { transform: translate3d(0, 46vh, 0) scaleX(0.2); opacity: 0; }
  18%  { opacity: 0.9; }
  56%  { transform: translate3d(0, -4vh, 0) scaleX(1); }
  86%  { transform: translate3d(0, 0, 0) scaleX(1); opacity: 0.9; }
  100% { opacity: 0; }
}

/* ===========================================================================
   8 · APIARIST — Comb                   .fx-moment-api-comb           2.5s
   Cells FILL IN, ring by ring outward from the centre. Built, not burst — the
   only moment that constructs something, and it constructs it in discrete rings
   so you can see the work being done.
   Markup: <i class="m-c m-ring g1"></i> x4 (g1..g4)
   =========================================================================== */
.fx-moment-api-comb .m-ring {
  border-radius: 50%;
  background:
    repeating-linear-gradient(0deg,   rgba(242, 115, 155, 0.5) 0 1.5px, transparent 1.5px 26px),
    repeating-linear-gradient(60deg,  rgba(242, 115, 155, 0.5) 0 1.5px, transparent 1.5px 26px),
    repeating-linear-gradient(120deg, rgba(255, 194, 122, 0.45) 0 1.5px, transparent 1.5px 26px);
  animation: api-comb-ring 1.5s cubic-bezier(.2, .8, .3, 1) forwards;
}
/* Each ring is a mask-cut annulus, so the fill genuinely arrives in bands
   rather than one disc growing. */
.fx-moment-api-comb .g1 { width: 150px;  height: 150px;  margin: -75px 0 0 -75px;
  -webkit-mask: radial-gradient(closest-side, #000 68%, transparent 72%);
          mask: radial-gradient(closest-side, #000 68%, transparent 72%); }
.fx-moment-api-comb .g2 { width: 290px;  height: 290px;  margin: -145px 0 0 -145px; animation-delay: 0.24s;
  -webkit-mask: radial-gradient(closest-side, transparent 46%, #000 50%, #000 82%, transparent 86%);
          mask: radial-gradient(closest-side, transparent 46%, #000 50%, #000 82%, transparent 86%); }
.fx-moment-api-comb .g3 { width: 460px;  height: 460px;  margin: -230px 0 0 -230px; animation-delay: 0.48s;
  -webkit-mask: radial-gradient(closest-side, transparent 60%, #000 64%, #000 88%, transparent 92%);
          mask: radial-gradient(closest-side, transparent 60%, #000 64%, #000 88%, transparent 92%); }
.fx-moment-api-comb .g4 { width: 680px;  height: 680px;  margin: -340px 0 0 -340px; animation-delay: 0.72s;
  -webkit-mask: radial-gradient(closest-side, transparent 70%, #000 74%, #000 92%, transparent 96%);
          mask: radial-gradient(closest-side, transparent 70%, #000 74%, #000 92%, transparent 96%); }
@keyframes api-comb-ring {
  0%   { transform: scale(0.86) rotate(-8deg); opacity: 0; }
  26%  { transform: scale(1) rotate(0deg); opacity: 1; }
  62%  { opacity: 1; }
  100% { transform: scale(1.04) rotate(4deg); opacity: 0; }
}

/* ===========================================================================
   9 · MYCOLOGIST — Bloom                .fx-moment-myco-bloom         2.6s
   The slowest moment in the app, and it should be: a cap SWELLS from nothing,
   holds, and only then releases a spore ring that expands past the edges. Two
   distinct acts with a pause between them — everything else here is one
   gesture.
   Markup: <i class="m-c m-cap"></i>, <i class="m-c m-spores"></i>
   =========================================================================== */
.fx-moment-myco-bloom .m-cap {
  width: min(26vw, 240px);
  height: min(26vw, 240px);
  margin: min(-13vw, -120px) 0 0 min(-13vw, -120px);
  border-radius: 50% 50% 44% 44%;
  background:
    repeating-conic-gradient(from 0deg, rgba(192, 132, 232, 0.3) 0deg 2deg, transparent 2deg 8deg),
    radial-gradient(closest-side at 50% 34%, rgba(255, 150, 132, 0.6), rgba(140, 63, 95, 0.35) 70%, transparent 88%);
  animation: myco-bloom-cap 2.2s cubic-bezier(.2, .68, .28, 1) forwards;
}
@keyframes myco-bloom-cap {
  0%   { transform: scale(0.04) translateY(18vh); opacity: 0; }
  10%  { opacity: 0.95; }
  /* swell */
  40%  { transform: scale(1) translateY(0); }
  /* hold — the pause between the two acts */
  62%  { transform: scale(1.02) translateY(0); opacity: 0.95; }
  100% { transform: scale(1.1) translateY(-2vh); opacity: 0; }
}
.fx-moment-myco-bloom .m-spores {
  width: 40px;
  height: 40px;
  margin: -20px 0 0 -20px;
  border-radius: 50%;
  border: 2px solid rgba(255, 150, 132, 0.6);
  box-shadow: 0 0 30px 6px rgba(192, 132, 232, 0.2);
  animation: myco-bloom-spores 1.5s cubic-bezier(.1, .72, .28, 1) 0.95s forwards;
}
@keyframes myco-bloom-spores {
  0%   { transform: scale(0.4); opacity: 0; }
  16%  { opacity: 0.9; }
  100% { transform: scale(26); opacity: 0; }
}

/* ===========================================================================
   10 · ALCHEMIST — Transmute            .fx-moment-alch-transmute     2.1s
   The only moment whose subject is a CHANGE OF STATE rather than a movement: a
   sigil draws itself, collapses inward to a point, and what comes back out is a
   different colour than what went in. Indigo goes in; glass-green comes out.
   Markup: <i class="m-c m-sigil"></i>, <i class="m-c m-flash"></i>
   =========================================================================== */
.fx-moment-alch-transmute .m-sigil {
  width: min(30vw, 280px);
  height: min(30vw, 280px);
  margin: min(-15vw, -140px) 0 0 min(-15vw, -140px);
  border-radius: 50%;
  border: 2px solid rgba(123, 139, 245, 0.7);
  background:
    linear-gradient(45deg, transparent 48%, rgba(123, 139, 245, 0.4) 49% 51%, transparent 52%),
    linear-gradient(-45deg, transparent 48%, rgba(123, 139, 245, 0.4) 49% 51%, transparent 52%),
    radial-gradient(closest-side, transparent 52%, rgba(123, 139, 245, 0.35) 54% 56%, transparent 58%);
  animation: alch-transmute-sigil 1.35s cubic-bezier(.5, .05, .6, 1) forwards;
}
@keyframes alch-transmute-sigil {
  0%   { transform: scale(0.5) rotate(-60deg); opacity: 0; }
  22%  { transform: scale(1) rotate(0deg); opacity: 1; }
  /* hold, drawn */
  52%  { transform: scale(1) rotate(12deg); opacity: 1; }
  /* collapse to a point */
  100% { transform: scale(0.02) rotate(180deg); opacity: 1; }
}
.fx-moment-alch-transmute .m-flash {
  width: 200vw;
  height: 200vh;
  margin: -100vh 0 0 -100vw;
  animation: alch-transmute-flash 0.8s cubic-bezier(.1, .8, .2, 1) 1.3s forwards;
}
/* What comes back out. The background changes hue family mid-animation, which is
   the whole point and the reason this is not a flash. */
@keyframes alch-transmute-flash {
  0%   { background: radial-gradient(closest-side, rgba(123, 139, 245, 0.7), transparent 20%); opacity: 0; }
  14%  { background: radial-gradient(closest-side, rgba(230, 255, 245, 0.8), transparent 30%); opacity: 1; }
  100% { background: radial-gradient(closest-side, rgba(103, 224, 168, 0.5), transparent 68%); opacity: 0; }
}

/* ===========================================================================
   11 · GLASSWORKER — Gather             .fx-moment-glass-gather       2.0s
   Shards fly INWARD and assemble into a lens, which then catches the light. The
   reverse of every burst in the app — and the reverse is the point: a
   glassworker's reward is a thing made whole, not a thing broken.
   Markup: <i class="m-c m-shard s1"></i> x8 (s1..s8), <i class="m-c m-lens"></i>
   =========================================================================== */
.fx-moment-glass-gather .m-shard {
  width: 3px;
  height: min(9vh, 80px);
  margin-top: min(-4.5vh, -40px);
  background: linear-gradient(0deg, transparent, rgba(214, 244, 252, 0.9), transparent);
  transform-origin: 50% 50%;
  animation: glass-gather-shard 1.1s cubic-bezier(.5, .02, .3, 1) forwards;
}
.fx-moment-glass-gather .s1 { --sa: 0deg;   }
.fx-moment-glass-gather .s2 { --sa: 45deg;  animation-delay: 0.03s; }
.fx-moment-glass-gather .s3 { --sa: 90deg;  animation-delay: 0.06s; }
.fx-moment-glass-gather .s4 { --sa: 135deg; animation-delay: 0.02s; }
.fx-moment-glass-gather .s5 { --sa: 180deg; animation-delay: 0.05s; }
.fx-moment-glass-gather .s6 { --sa: 225deg; animation-delay: 0.08s; }
.fx-moment-glass-gather .s7 { --sa: 270deg; animation-delay: 0.01s; }
.fx-moment-glass-gather .s8 { --sa: 315deg; animation-delay: 0.04s; }
@keyframes glass-gather-shard {
  0%   { transform: rotate(var(--sa, 0deg)) translateY(-70vh) scaleY(1.6); opacity: 0; }
  20%  { opacity: 1; }
  100% { transform: rotate(var(--sa, 0deg)) translateY(-9vh) scaleY(0.5); opacity: 0.9; }
}
.fx-moment-glass-gather .m-lens {
  width: min(22vw, 200px);
  height: min(22vw, 200px);
  margin: min(-11vw, -100px) 0 0 min(-11vw, -100px);
  border-radius: 50%;
  background:
    radial-gradient(circle at 38% 32%, rgba(255, 255, 255, 0.75) 0 6%, transparent 24%),
    radial-gradient(closest-side, rgba(99, 220, 237, 0.35), rgba(99, 220, 237, 0.08) 70%, transparent 88%);
  animation: glass-gather-lens 0.9s cubic-bezier(.1, .85, .2, 1) 1.05s forwards;
}
@keyframes glass-gather-lens {
  0%   { transform: scale(0.3); opacity: 0; }
  34%  { transform: scale(1.02); opacity: 1; }
  100% { transform: scale(1.35); opacity: 0; }
}

/* ===========================================================================
   12 · FORGEWRIGHT — Quench             .fx-moment-forge-quench       1.8s
   White-hot, then WATER. The flash does not fade — it CONTRACTS to a dark disc
   while steam sheets outward sideways, so the moment reads as heat being taken
   out of something rather than as light going away. The loudest opening and the
   quietest ending of any moment here.
   Markup: <i class="m-c m-heat"></i>, <i class="m-steam"></i>
   =========================================================================== */
.fx-moment-forge-quench .m-heat {
  width: 220vmax;
  height: 220vmax;
  margin: -110vmax 0 0 -110vmax;
  border-radius: 50%;
  animation: forge-quench-heat 1.15s cubic-bezier(.3, .02, .5, 1) forwards;
}
@keyframes forge-quench-heat {
  0%   { transform: scale(0.02);
         background: radial-gradient(closest-side, #fff8e8, rgba(255, 214, 150, 0.7) 40%, transparent 62%);
         opacity: 0; }
  9%   { transform: scale(0.5); opacity: 1; }
  /* orange, spread */
  38%  { transform: scale(1);
         background: radial-gradient(closest-side, rgba(255, 138, 60, 0.85), rgba(255, 106, 43, 0.35) 44%, transparent 66%); }
  /* contract, and go dark rather than transparent */
  100% { transform: scale(0.16);
         background: radial-gradient(closest-side, rgba(60, 20, 12, 0.9), rgba(30, 10, 6, 0.5) 50%, transparent 70%);
         opacity: 0.9; }
}
.fx-moment-forge-quench .m-steam {
  left: -10%;
  right: -10%;
  top: 50%;
  height: min(26vh, 240px);
  margin-top: min(-13vh, -120px);
  background:
    radial-gradient(closest-side at 20% 50%, rgba(236, 236, 230, 0.4), transparent 72%),
    radial-gradient(closest-side at 54% 44%, rgba(236, 236, 230, 0.34), transparent 72%),
    radial-gradient(closest-side at 82% 56%, rgba(236, 236, 230, 0.38), transparent 72%);
  animation: forge-quench-steam 1.5s cubic-bezier(.08, .8, .3, 1) 0.28s forwards;
}
/* Sideways, hard — a quench throws steam out, not up. */
@keyframes forge-quench-steam {
  0%   { transform: scale(0.1, 0.5); opacity: 0; }
  18%  { opacity: 0.95; }
  100% { transform: scale(2.6, 1.4); opacity: 0; }
}

/* ===========================================================================
   13 · WEAVER — Tapestry                .fx-moment-weave-tapestry     2.4s
   Warp drops in vertically, weft sweeps in horizontally, and only where they
   cross does a figure resolve. Three elements, three directions, and the reward
   is the INTERSECTION — no other moment in the app has a payoff that depends on
   two earlier parts having arrived.
   Markup: <i class="m-warp"></i>, <i class="m-weft"></i>, <i class="m-c m-figure"></i>
   =========================================================================== */
.fx-moment-weave-tapestry .m-warp {
  inset: 0;
  background: repeating-linear-gradient(90deg, rgba(226, 99, 189, 0.4) 0 2px, transparent 2px 22px);
  transform-origin: 50% 0;
  animation: weave-tapestry-warp 1.9s cubic-bezier(.2, .8, .3, 1) forwards;
}
@keyframes weave-tapestry-warp {
  0%   { transform: scaleY(0); opacity: 0; }
  12%  { opacity: 0.9; }
  46%  { transform: scaleY(1); }
  78%  { opacity: 0.9; }
  100% { transform: scaleY(1); opacity: 0; }
}
.fx-moment-weave-tapestry .m-weft {
  inset: 0;
  background: repeating-linear-gradient(0deg, rgba(245, 143, 212, 0.34) 0 2px, transparent 2px 22px);
  transform-origin: 0 50%;
  animation: weave-tapestry-weft 1.9s cubic-bezier(.2, .8, .3, 1) 0.3s forwards;
}
@keyframes weave-tapestry-weft {
  0%   { transform: scaleX(0); opacity: 0; }
  12%  { opacity: 0.9; }
  46%  { transform: scaleX(1); }
  74%  { opacity: 0.9; }
  100% { transform: scaleX(1); opacity: 0; }
}
.fx-moment-weave-tapestry .m-figure {
  width: min(34vw, 320px);
  height: min(34vw, 320px);
  margin: min(-17vw, -160px) 0 0 min(-17vw, -160px);
  background: conic-gradient(from 45deg at 50% 50%,
    rgba(226, 99, 189, 0.42) 0deg 90deg, transparent 90deg 180deg,
    rgba(147, 217, 168, 0.34) 180deg 270deg, transparent 270deg 360deg);
  animation: weave-tapestry-figure 1.1s cubic-bezier(.15, .8, .3, 1) 1.05s forwards;
}
@keyframes weave-tapestry-figure {
  0%   { transform: scale(0.7); opacity: 0; }
  38%  { transform: scale(1); opacity: 1; }
  100% { transform: scale(1.08); opacity: 0; }
}

/* ===========================================================================
   14 · COOK — Served                    .fx-moment-cook-served        2.0s
   A plate arrives. The ring DRAWS itself round (a conic sweep, not a scale),
   steam curls off it, and then the whole thing LIFTS out of frame — the only
   moment that exits upward as a single object rather than dissipating.
   Markup: <i class="m-c m-plate"></i>, <i class="m-c m-curl"></i>
   =========================================================================== */
.fx-moment-cook-served .m-plate {
  width: min(28vw, 260px);
  height: min(28vw, 260px);
  margin: min(-14vw, -130px) 0 0 min(-14vw, -130px);
  border-radius: 50%;
  background: conic-gradient(from -90deg, rgba(240, 212, 136, 0.8) 0deg, rgba(158, 207, 66, 0.5) 200deg, transparent 0deg);
  -webkit-mask: radial-gradient(closest-side, transparent 76%, #000 79%);
          mask: radial-gradient(closest-side, transparent 76%, #000 79%);
  animation: cook-served-plate 1.75s cubic-bezier(.3, .05, .4, 1) forwards;
}
/* The sweep is the angle of the conic stop, so the ring genuinely draws round
   rather than growing. */
@keyframes cook-served-plate {
  0%   { background: conic-gradient(from -90deg, rgba(240, 212, 136, 0.8) 0deg, transparent 0deg);
         transform: translateY(0); opacity: 0; }
  8%   { opacity: 1; }
  44%  { background: conic-gradient(from -90deg, rgba(240, 212, 136, 0.8) 0deg, rgba(158, 207, 66, 0.5) 300deg, transparent 360deg);
         transform: translateY(0); }
  62%  { transform: translateY(0); }
  100% { background: conic-gradient(from -90deg, rgba(240, 212, 136, 0.8) 0deg, rgba(158, 207, 66, 0.5) 300deg, transparent 360deg);
         transform: translateY(-46vh); opacity: 0; }
}
.fx-moment-cook-served .m-curl {
  width: min(9vw, 84px);
  height: min(9vw, 84px);
  margin: min(-4.5vw, -42px) 0 0 min(-4.5vw, -42px);
  border-radius: 50%;
  border: 5px solid rgba(240, 236, 214, 0.34);
  border-right-color: transparent;
  border-bottom-color: transparent;
  animation: cook-served-curl 1.6s cubic-bezier(.25, .1, .4, 1) 0.4s forwards;
}
@keyframes cook-served-curl {
  0%   { transform: translateY(6vh) rotate(0deg) scale(0.4); opacity: 0; }
  20%  { opacity: 0.9; }
  100% { transform: translateY(-34vh) rotate(400deg) scale(2.3); opacity: 0; }
}

/* ===========================================================================
   15 · SCRIBE — Illuminate              .fx-moment-scribe-illuminate  2.3s
   Gold leaf laid on a capital. A gilded block WIPES in from the top-left corner
   — not from the centre, which every other moment here uses — and the ruling
   draws out of it afterwards. The quiet element's loudest moment is gold, and it
   is still quiet.
   Markup: <i class="m-leaf"></i>, <i class="m-rule ru1"></i> x3 (ru1..ru3)
   =========================================================================== */
.fx-moment-scribe-illuminate .m-leaf {
  left: 12vw;
  top: 16vh;
  width: min(24vw, 220px);
  height: min(24vw, 220px);
  background:
    linear-gradient(122deg, rgba(255, 243, 196, 0.6) 0 22%, rgba(221, 196, 140, 0.7) 34% 52%, rgba(140, 106, 36, 0.5) 68%, rgba(221, 196, 140, 0.6));
  -webkit-mask: linear-gradient(122deg, #000 0 50%, transparent 50%);
          mask: linear-gradient(122deg, #000 0 50%, transparent 50%);
  -webkit-mask-size: 300% 300%;
          mask-size: 300% 300%;
  animation: scribe-illuminate-leaf 2.0s cubic-bezier(.3, .04, .4, 1) forwards;
}
/* The wipe is the MASK moving, so the gold is revealed rather than grown. */
@keyframes scribe-illuminate-leaf {
  0%   { -webkit-mask-position: 100% 100%; mask-position: 100% 100%; opacity: 0; }
  10%  { opacity: 1; }
  54%  { -webkit-mask-position: 0% 0%; mask-position: 0% 0%; opacity: 1; }
  80%  { opacity: 1; }
  100% { -webkit-mask-position: 0% 0%; mask-position: 0% 0%; opacity: 0; }
}
.fx-moment-scribe-illuminate .m-rule {
  left: calc(12vw + min(24vw, 220px));
  height: 2px;
  background: linear-gradient(90deg, rgba(221, 196, 140, 0.75), rgba(139, 164, 201, 0.4), transparent);
  transform-origin: 0 50%;
  animation: scribe-illuminate-rule 1.2s cubic-bezier(.2, .8, .3, 1) forwards;
}
.fx-moment-scribe-illuminate .ru1 { top: calc(16vh + 3vw);  width: 44vw; animation-delay: 0.62s; }
.fx-moment-scribe-illuminate .ru2 { top: calc(16vh + 9vw);  width: 38vw; animation-delay: 0.78s; }
.fx-moment-scribe-illuminate .ru3 { top: calc(16vh + 15vw); width: 41vw; animation-delay: 0.94s; }
@keyframes scribe-illuminate-rule {
  0%   { transform: scaleX(0); opacity: 0; }
  16%  { opacity: 0.9; }
  60%  { transform: scaleX(1); }
  100% { transform: scaleX(1); opacity: 0; }
}


/* ===========================================================================
   PASS 3 of 4: PAGE-SLOT PLACEMENTS.

   FOUR PER PROFESSION, NOT TEN. The full grid is 150 engines, and a shop with
   150 rows is not a richer shop — it is one where nothing is chosen because
   nothing is distinct. Each profession takes the four surfaces where its craft
   has something to say, and the four are picked so that all ten slots get six
   professions each. A Prospector has an opinion about a title, a currency, a
   card and a selected card; it has nothing to say about a toggle, so it does not
   ship one.

   Bespoke throughout, per the directive: no rule below could be replaced by
   adding a class to an effects.css engine. Where two professions land on the
   same slot they use different mechanisms, not the same one recoloured — the
   Woodwright's progress bar animates its TRACK, the Tidecaller's animates the
   FILL's leading edge, the Forgewright's animates the fill's COLOUR.

   Targets are the app's real selectors, taken from effects.css's own bindings:
     sidebar    .sidebar
     title      .page-header h1
     nav        .nav-item.active
     avatar     .account-avatar, .profile-avatar
     currency   .coin-total, .coin-clicker
     progress   .xp-fill / .quest-bar-fill  ·  .xp-bar / .quest-bar
     card       .record-card, .loot-card, .quest-card, .shared-card, .codex-card
     cardState  .record-card.selected · .record-card.is-need ·
                .quest-card:not(.not-started):not(.complete) · .loot-rarity.lr-legendary
     chip       .chip, .cat-dot, .loot-chip
     toggle     .blk-step.done .blk-tick, .chip

   TWO RULES FROM effects.css THAT STILL BITE HERE:
   1. A cosmetic must not change layout for someone not wearing it. Every rule
      hangs off html[data-on-*]; nothing below is unscoped.
   2. Anything that puts a layer inside a card must lift the card's own children,
      and the :not(.fx-layer) guard is load-bearing — without it the rule hits the
      particle layer itself and collapses it to a zero-height flex item.
   =========================================================================== */

/* ============ 1 · PROSPECTOR ============ */

/* title — Ore Seam. A hard-edged vein crossing the glyphs. STATIC: a sweep here
   would be the gilded title again, and the whole point of a seam is that it was
   already in the rock. */
html[data-on-pros-seam] .page-header h1 {
  background-image: linear-gradient(74deg,
    rgba(140, 106, 52, 0.9) 0 38%, rgba(255, 232, 180, 0.95) 41% 44%,
    rgba(232, 161, 60, 0.9) 47% 58%, rgba(140, 106, 52, 0.9) 62%);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}

/* currency — Assay. The pill WEIGHS what it is given: it tips a degree and
   settles. Fires on the class the counter already gets when it changes, so
   nothing new has to be written into the page that draws it. */
html[data-on-pros-assay] .coin-total { transform-origin: 18% 50%; transition: transform 0.5s cubic-bezier(.2, 1.5, .4, 1); }
html[data-on-pros-assay] .coin-total.is-earn { transform: rotate(-1.6deg); }

/* card — Core Log. A banded column at the card's inner edge, read like a core
   sample. Unequal band heights, or it is a barcode. */
html[data-on-pros-corelog] .record-card,
html[data-on-pros-corelog] .loot-card,
html[data-on-pros-corelog] .quest-card,
html[data-on-pros-corelog] .shared-card,
html[data-on-pros-corelog] .codex-card { position: relative; }
html[data-on-pros-corelog] .record-card::after,
html[data-on-pros-corelog] .loot-card::after,
html[data-on-pros-corelog] .quest-card::after,
html[data-on-pros-corelog] .shared-card::after,
html[data-on-pros-corelog] .codex-card::after {
  content: ""; position: absolute; left: 4px; top: 7px; bottom: 7px; width: 3px;
  border-radius: 2px; pointer-events: none;
  background: linear-gradient(180deg,
    rgba(232, 161, 60, 0.55) 0 22%, rgba(120, 96, 72, 0.5) 22% 31%,
    rgba(255, 210, 122, 0.5) 31% 47%, rgba(120, 96, 72, 0.5) 47% 68%,
    rgba(232, 161, 60, 0.45) 68% 79%, rgba(120, 96, 72, 0.4) 79%);
}

/* cardState — Facet. ONLY the selected card: its corners are cut off and the cut
   catches the light, which turns to a slow angle. A gem is faceted, not glowing. */
html[data-on-pros-facet] .record-card.selected,
html[data-on-pros-facet] .quest-card.selected {
  clip-path: polygon(9px 0, 100% 0, 100% calc(100% - 9px), calc(100% - 9px) 100%, 0 100%, 0 9px);
  position: relative;
}
html[data-on-pros-facet] .record-card.selected::before,
html[data-on-pros-facet] .quest-card.selected::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: conic-gradient(from var(--fx-ang, 0deg) at 50% 50%,
    transparent 0deg 30deg, rgba(255, 232, 180, 0.11) 44deg 58deg,
    transparent 74deg 200deg, rgba(232, 161, 60, 0.08) 216deg 232deg, transparent 250deg);
  animation: pros-facet-turn 11s linear infinite;
}
@keyframes pros-facet-turn { to { --fx-ang: 360deg; } }

/* ============ 2 · HERBALIST ============ */

/* sidebar — Creeper. A vine GROWS down the rail's inner edge and stops, budding
   at intervals. It reaches full height in twelve seconds and then simply stays —
   a plant does not loop. */
html[data-on-herb-creeper] .sidebar { position: relative; overflow: hidden; }
html[data-on-herb-creeper] .sidebar::before {
  content: ""; position: absolute; top: 0; right: 3px; width: 2px; pointer-events: none;
  border-radius: 2px;
  background: linear-gradient(180deg, rgba(116, 207, 138, 0.75), rgba(116, 207, 138, 0.3));
  animation: herb-creeper-grow 12s cubic-bezier(.3, .7, .4, 1) forwards;
}
@keyframes herb-creeper-grow { from { height: 0; } to { height: 100%; } }
html[data-on-herb-creeper] .sidebar::after {
  content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 9px; pointer-events: none;
  background: radial-gradient(ellipse 5px 3px at 100% 14%, rgba(116, 207, 138, 0.5) 0 60%, transparent 64%),
              radial-gradient(ellipse 5px 3px at 0% 34%, rgba(116, 207, 138, 0.45) 0 60%, transparent 64%),
              radial-gradient(ellipse 5px 3px at 100% 56%, rgba(116, 207, 138, 0.5) 0 60%, transparent 64%),
              radial-gradient(ellipse 5px 3px at 0% 78%, rgba(116, 207, 138, 0.4) 0 60%, transparent 64%);
  animation: herb-creeper-bud 12s steps(4, end) forwards;
}
@keyframes herb-creeper-bud { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0); } }

/* card — Pressed. The card reads as a herbarium sheet: a leaf silhouette laid in
   the corner and two paper tapes holding it. Entirely static — a pressed
   specimen that moves is a live one. */
html[data-on-herb-press] .record-card,
html[data-on-herb-press] .loot-card,
html[data-on-herb-press] .codex-card { position: relative; }
html[data-on-herb-press] .record-card::after,
html[data-on-herb-press] .loot-card::after,
html[data-on-herb-press] .codex-card::after {
  content: ""; position: absolute; right: 6px; bottom: 5px; width: 34px; height: 20px;
  pointer-events: none; opacity: 0.5;
  background:
    radial-gradient(ellipse 40% 16% at 30% 42%, rgba(116, 207, 138, 0.55) 0 62%, transparent 66%),
    radial-gradient(ellipse 16% 34% at 62% 32%, rgba(116, 207, 138, 0.45) 0 62%, transparent 66%),
    radial-gradient(ellipse 34% 13% at 74% 68%, rgba(116, 207, 138, 0.4) 0 62%, transparent 66%),
    linear-gradient(58deg, transparent 47%, rgba(116, 207, 138, 0.4) 48% 52%, transparent 53%);
}
html[data-on-herb-press] .record-card::before,
html[data-on-herb-press] .loot-card::before,
html[data-on-herb-press] .codex-card::before {
  content: ""; position: absolute; left: -3px; top: 12px; width: 16px; height: 7px;
  pointer-events: none; transform: rotate(-24deg);
  background: rgba(226, 214, 168, 0.14);
  box-shadow: 0 0 0 0.5px rgba(226, 214, 168, 0.12);
}

/* chip — Graft. A stem and one leaf at the chip's left end, drawn in the gap the
   pill's padding already provides. No motion; a tag is a label. */
html[data-on-herb-graft] .chip,
html[data-on-herb-graft] .loot-chip { position: relative; padding-left: 15px; }
html[data-on-herb-graft] .chip::before,
html[data-on-herb-graft] .loot-chip::before {
  content: ""; position: absolute; left: 5px; top: 50%; width: 6px; height: 6px;
  margin-top: -3px; pointer-events: none;
  border-left: 1px solid rgba(116, 207, 138, 0.8);
  border-bottom: 1px solid rgba(116, 207, 138, 0.8);
  border-bottom-left-radius: 5px;
}

/* toggle — Sprout. ON TICK: a shoot pushes up out of the box and opens one leaf.
   Grows, it does not pop. */
html[data-on-herb-sprout] .blk-tick { position: relative; overflow: visible; }
html[data-on-herb-sprout] .blk-step.done .blk-tick::after {
  content: ""; position: absolute; left: 50%; bottom: 60%; width: 1.5px; margin-left: -0.75px;
  background: rgba(116, 207, 138, 0.9); pointer-events: none; transform-origin: 50% 100%;
  animation: herb-sprout-shoot 0.7s cubic-bezier(.2, .9, .3, 1) forwards;
}
@keyframes herb-sprout-shoot {
  0%   { height: 0; opacity: 0; }
  20%  { opacity: 1; }
  70%  { height: 11px; }
  100% { height: 10px; opacity: 0; }
}

/* ============ 3 · WOODWRIGHT ============ */

/* progress — Kerf. The UNFILLED part of the bar is a saw cut: teeth along its top
   edge, and sawdust gathering where the blade is. Animates the TRACK, which is
   the half of a bar nothing else here touches. */
html[data-on-wood-kerf] .xp-bar,
html[data-on-wood-kerf] .quest-bar { position: relative; overflow: hidden; }
html[data-on-wood-kerf] .xp-bar::after,
html[data-on-wood-kerf] .quest-bar::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background-image:
    repeating-linear-gradient(-58deg, rgba(201, 138, 69, 0.55) 0 1px, transparent 1px 5px),
    linear-gradient(90deg, transparent calc(var(--fill, 62%) - 6px), rgba(201, 138, 69, 0.3) var(--fill, 62%), transparent calc(var(--fill, 62%) + 9px));
  background-size: 5px 3px, 100% 100%;
  background-position: 0 0, 0 0;
  background-repeat: repeat-x, no-repeat;
  animation: wood-kerf-cut 1.1s linear infinite;
}
@keyframes wood-kerf-cut { to { background-position: 5px 0, 0 0; } }

/* card — Joint. Dovetail notches cut into two corners. Static, and the notches
   are UNEQUAL because a dovetail is directional. */
html[data-on-wood-joint] .record-card,
html[data-on-wood-joint] .loot-card,
html[data-on-wood-joint] .quest-card,
html[data-on-wood-joint] .shared-card,
html[data-on-wood-joint] .codex-card {
  clip-path: polygon(
    0 0, 22px 0, 26px 5px, 44px 5px, 48px 0, 100% 0,
    100% 100%, 48px 100%, 44px calc(100% - 5px), 26px calc(100% - 5px), 22px 100%, 0 100%);
}

/* nav — Peg. The current item is held by a dowel with visible grain, not lit by a
   glow. */
html[data-on-wood-peg] .nav-item.active { position: relative; }
html[data-on-wood-peg] .nav-item.active::before {
  content: ""; position: absolute; left: 2px; top: 50%; width: 5px; height: 5px;
  margin-top: -2.5px; border-radius: 50%; pointer-events: none;
  background:
    repeating-radial-gradient(circle at 50% 50%, rgba(120, 78, 38, 0.9) 0 0.5px, rgba(201, 138, 69, 0.9) 0.5px 1.4px),
    rgba(201, 138, 69, 0.9);
  box-shadow: 0 0 0 0.5px rgba(120, 78, 38, 0.6);
}

/* title — Planed. Grain running through the letters, cut across at one point by a
   plane's pass. Static. */
html[data-on-wood-planed] .page-header h1 {
  background-image:
    repeating-linear-gradient(4deg, rgba(120, 78, 38, 0.95) 0 2px, rgba(201, 138, 69, 0.95) 2px 5px),
    linear-gradient(0deg, rgba(236, 214, 180, 0.95), rgba(236, 214, 180, 0.95));
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}

/* ============ 4 · TIDECALLER ============ */

/* sidebar — Waterline. The rail has a level in it, and the level RISES AND FALLS
   over a tide-length cycle. Everything below the line is a shade cooler. */
html[data-on-tide-waterline] .sidebar { position: relative; overflow: hidden; }
html[data-on-tide-waterline] .sidebar::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 120%;
  pointer-events: none;
  background:
    linear-gradient(0deg, rgba(63, 169, 216, 0.16) 0 calc(100% - 2px), rgba(127, 227, 224, 0.5) calc(100% - 2px));
  animation: tide-waterline 38s ease-in-out infinite alternate;
}
@keyframes tide-waterline {
  from { transform: translate3d(0, 46%, 0); }
  to   { transform: translate3d(0, 18%, 0); }
}

/* progress — Meniscus. The FILL's leading edge is a curved surface that wobbles,
   the way liquid in a tube does. The fill body is untouched. */
html[data-on-tide-meniscus] .xp-fill,
html[data-on-tide-meniscus] .quest-bar-fill { position: relative; overflow: visible; }
html[data-on-tide-meniscus] .xp-fill::after,
html[data-on-tide-meniscus] .quest-bar-fill::after {
  content: ""; position: absolute; right: -3px; top: -1px; bottom: -1px; width: 8px;
  pointer-events: none; border-radius: 50% / 40%;
  background: linear-gradient(90deg, rgba(127, 227, 224, 0.7), rgba(215, 248, 252, 0.9));
  animation: tide-meniscus 2.9s ease-in-out infinite alternate;
}
@keyframes tide-meniscus {
  from { transform: scaleY(0.82) translateX(-1px); }
  to   { transform: scaleY(1.12) translateX(1.5px); }
}

/* cardState — Ripple. ONLY cards you have not collected: their dashed border
   becomes a ring travelling OUTWARD from the card and dissipating, so the card
   reads as something dropped in water. */
html[data-on-tide-ripple] .record-card.is-need { position: relative; }
html[data-on-tide-ripple] .record-card.is-need::before,
html[data-on-tide-ripple] .record-card.is-need::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  border: 1px solid rgba(127, 227, 224, 0.5);
  animation: tide-ripple-out 3.6s cubic-bezier(.2, .6, .3, 1) infinite;
}
html[data-on-tide-ripple] .record-card.is-need::after { animation-delay: 1.8s; }
@keyframes tide-ripple-out {
  0%   { transform: scale(1); opacity: 0; }
  14%  { opacity: 0.7; }
  100% { transform: scale(1.06); opacity: 0; }
}

/* avatar — Half-submerged. The badge sits in water: its lower part is tinted and
   the waterline crosses it, moving. Overlay only — nothing on the badge root, so
   no stacking context and rank rings still work. */
html[data-on-tide-submerged] .account-avatar,
html[data-on-tide-submerged] .profile-avatar { position: relative; }
html[data-on-tide-submerged] .account-avatar::after,
html[data-on-tide-submerged] .profile-avatar::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%; overflow: hidden;
  pointer-events: none;
  background: linear-gradient(0deg, rgba(63, 169, 216, 0.42) 0 52%, rgba(215, 248, 252, 0.65) 52% 54%, transparent 54%);
  animation: tide-submerged 6.5s ease-in-out infinite alternate;
}
@keyframes tide-submerged {
  from { background-position: 0 3px; }
  to   { background-position: 0 -3px; }
}

/* ============ 5 · BONE-HUNTER ============ */

/* nav — Spoor. The current item is marked by a PRINT beside it, not by light: two
   pads and a heel, static, and offset from the item as a track would be. */
html[data-on-bone-spoor] .nav-item.active { position: relative; }
html[data-on-bone-spoor] .nav-item.active::after {
  content: ""; position: absolute; right: 7px; top: 50%; width: 9px; height: 8px;
  margin-top: -4px; pointer-events: none; opacity: 0.6;
  background:
    radial-gradient(circle 1.4px at 15% 22%, rgba(226, 226, 234, 0.95) 0 100%, transparent 0),
    radial-gradient(circle 1.4px at 52% 8%, rgba(226, 226, 234, 0.95) 0 100%, transparent 0),
    radial-gradient(circle 1.4px at 88% 26%, rgba(226, 226, 234, 0.95) 0 100%, transparent 0),
    radial-gradient(ellipse 42% 26% at 52% 78%, rgba(226, 226, 234, 0.85) 0 100%, transparent 0);
}

/* cardState — Scavenged. ONLY uncollected cards: the border is replaced by short
   rib strokes, evenly spaced round the card and NOT moving. Marching ants say
   "working on it"; this says "picked clean". */
html[data-on-bone-scavenged] .record-card.is-need {
  border-color: transparent !important;
  background-image:
    repeating-linear-gradient(90deg, rgba(206, 206, 216, 0.65) 0 1.5px, transparent 1.5px 9px),
    repeating-linear-gradient(90deg, rgba(206, 206, 216, 0.65) 0 1.5px, transparent 1.5px 9px),
    repeating-linear-gradient(0deg, rgba(206, 206, 216, 0.45) 0 1.5px, transparent 1.5px 9px),
    repeating-linear-gradient(0deg, rgba(206, 206, 216, 0.45) 0 1.5px, transparent 1.5px 9px);
  background-size: 9px 4px, 9px 4px, 4px 9px, 4px 9px;
  background-position: 0 0, 0 100%, 0 0, 100% 0;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
}

/* chip — Notched. A bite out of the pill's trailing edge. Static, and it makes
   the tag read as something taken from rather than something added to. */
html[data-on-bone-notched] .chip,
html[data-on-bone-notched] .loot-chip {
  clip-path: polygon(0 0, calc(100% - 8px) 0, calc(100% - 3px) 34%, 100% 50%, calc(100% - 3px) 66%, calc(100% - 8px) 100%, 0 100%);
}

/* title — Etched. The heading is scratched into the ground: a hairline copy of the
   letters offset behind them, in the cold accent. No fill change, so contrast is
   untouched. */
html[data-on-bone-etched] .page-header h1 {
  text-shadow:
    0.5px 0.5px 0 rgba(206, 206, 216, 0.45),
    -1px -1px 0 rgba(0, 0, 0, 0.55);
}

/* ============ 6 · BEASTMASTER ============ */

/* avatar — Hackles. Spines rise around the badge ON HOVER and settle when you
   leave. A transform on a ring, never on the badge. */
html[data-on-beast-hackles] .account-avatar,
html[data-on-beast-hackles] .profile-avatar { position: relative; }
html[data-on-beast-hackles] .account-avatar::before,
html[data-on-beast-hackles] .profile-avatar::before {
  content: ""; position: absolute; inset: -6px; border-radius: 50%; pointer-events: none;
  background: repeating-conic-gradient(from 0deg,
    rgba(176, 122, 240, 0.85) 0deg 2deg, transparent 2deg 13deg);
  -webkit-mask: radial-gradient(closest-side, transparent 70%, #000 74%);
          mask: radial-gradient(closest-side, transparent 70%, #000 74%);
  transform: scale(0.86); opacity: 0;
  transition: transform 0.28s cubic-bezier(.2, 1.4, .4, 1), opacity 0.2s;
}
html[data-on-beast-hackles] .account-avatar:hover::before,
html[data-on-beast-hackles] .profile-avatar:hover::before { transform: scale(1); opacity: 0.9; }

/* toggle — Snap. ON TICK: shuts fast and hard with a recoil, in 180ms. Every other
   toggle in the app overshoots gently; this one bites. */
html[data-on-beast-snap] .blk-step.done .blk-tick,
html[data-on-beast-snap] .chip.is-on {
  animation: beast-snap 0.18s cubic-bezier(.9, -0.4, .2, 1.9);
}
@keyframes beast-snap {
  0%   { transform: scale(1.22) rotate(-4deg); }
  55%  { transform: scale(0.9) rotate(3deg); }
  100% { transform: scale(1) rotate(0deg); }
}

/* cardState — Stalked. ONLY the selected card: a DARK arc travels round its edge —
   something circling it. Everything else in the app lights a selected card up;
   this one puts a shadow on it. */
html[data-on-beast-stalked] .record-card.selected,
html[data-on-beast-stalked] .quest-card.selected { position: relative; }
html[data-on-beast-stalked] .record-card.selected::before,
html[data-on-beast-stalked] .quest-card.selected::before {
  content: ""; position: absolute; inset: -2px; border-radius: inherit; pointer-events: none;
  padding: 2px;
  background: conic-gradient(from var(--fx-ang, 0deg),
    transparent 0deg 280deg, rgba(10, 6, 14, 0.9) 330deg, rgba(176, 122, 240, 0.5) 352deg, transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  animation: beast-stalked-circle 6s linear infinite;
}
@keyframes beast-stalked-circle { to { --fx-ang: 360deg; } }

/* nav — Scent. A fading trail BEHIND the current item, pointing back the way you
   came. Static gradient, no animation: a trail is a record, not an event. */
html[data-on-beast-scent] .nav-item.active { position: relative; }
html[data-on-beast-scent] .nav-item.active::after {
  content: ""; position: absolute; left: 10px; right: 10px; top: -7px; height: 6px;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(176, 122, 240, 0.28) 60%, transparent);
  -webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 8px);
          mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 8px);
}

/* ============ 7 · HOMESTEADER ============ */

/* progress — Rows. The bar fills in DISCRETE PLANTED ROWS rather than smoothly —
   steps(), because a field is sown one row at a time. */
html[data-on-home-rows] .xp-fill,
html[data-on-home-rows] .quest-bar-fill { position: relative; overflow: hidden; }
html[data-on-home-rows] .xp-fill::after,
html[data-on-home-rows] .quest-bar-fill::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background-image: repeating-linear-gradient(90deg,
    rgba(226, 214, 168, 0.45) 0 1.5px, transparent 1.5px 8px);
  animation: home-rows-sow 3.2s steps(8, end) infinite;
}
@keyframes home-rows-sow { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0); } }

/* card — Shelf. The card SITS on a drawn line with a small contact shadow. It is
   the only card treatment in the app that is about weight rather than surface. */
html[data-on-home-shelf] .record-card,
html[data-on-home-shelf] .loot-card,
html[data-on-home-shelf] .codex-card { position: relative; }
html[data-on-home-shelf] .record-card::after,
html[data-on-home-shelf] .loot-card::after,
html[data-on-home-shelf] .codex-card::after {
  content: ""; position: absolute; left: 6%; right: 6%; bottom: -3px; height: 3px;
  pointer-events: none; border-radius: 2px;
  background:
    linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.4) 20% 80%, transparent),
    linear-gradient(0deg, rgba(106, 167, 156, 0.5) 0 1px, transparent 1px);
}

/* chip — Preserve. The tag is a jar label: a paper band with the lid's arc above
   it. Static. */
html[data-on-home-preserve] .chip,
html[data-on-home-preserve] .loot-chip {
  border-radius: 3px; position: relative;
  background-image: repeating-linear-gradient(0deg, rgba(226, 214, 168, 0.06) 0 1px, transparent 1px 3px);
}
html[data-on-home-preserve] .chip::before,
html[data-on-home-preserve] .loot-chip::before {
  content: ""; position: absolute; left: 20%; right: 20%; top: -3px; height: 4px;
  pointer-events: none; border-radius: 3px 3px 0 0;
  background: rgba(106, 167, 156, 0.45);
}

/* sidebar — Fenceline. The rail's outer edge is a fence: posts at intervals with
   two rails between them. Nothing moves. */
html[data-on-home-fenceline] .sidebar { position: relative; }
html[data-on-home-fenceline] .sidebar::after {
  content: ""; position: absolute; top: 0; bottom: 0; right: 0; width: 7px; pointer-events: none;
  background-image:
    repeating-linear-gradient(0deg, rgba(106, 167, 156, 0.6) 0 2px, transparent 2px 34px),
    linear-gradient(90deg, transparent 2px, rgba(106, 167, 156, 0.4) 2px 3px, transparent 3px 5px, rgba(106, 167, 156, 0.3) 5px 6px, transparent 6px);
  background-size: 100% 34px, 100% 100%;
}

/* ============ 8 · APIARIST ============ */

/* avatar — Veil. A fine hex mesh drawn OVER the badge, as a keeper's veil sits
   between you and the hive. Overlay only; nothing on .bdg. */
html[data-on-api-veil] .account-avatar,
html[data-on-api-veil] .profile-avatar { position: relative; }
html[data-on-api-veil] .account-avatar::after,
html[data-on-api-veil] .profile-avatar::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%; pointer-events: none;
  opacity: 0.5;
  background:
    repeating-linear-gradient(0deg,   rgba(255, 222, 190, 0.7) 0 0.5px, transparent 0.5px 5px),
    repeating-linear-gradient(60deg,  rgba(255, 222, 190, 0.7) 0 0.5px, transparent 0.5px 5px),
    repeating-linear-gradient(120deg, rgba(255, 222, 190, 0.7) 0 0.5px, transparent 0.5px 5px);
}

/* chip — Cell. The pill's ends are cut to hex points instead of rounded, so a row
   of tags reads as comb. */
html[data-on-api-cell] .chip,
html[data-on-api-cell] .loot-chip {
  border-radius: 0;
  clip-path: polygon(7px 0, calc(100% - 7px) 0, 100% 50%, calc(100% - 7px) 100%, 7px 100%, 0 50%);
  padding-left: 12px; padding-right: 12px;
}

/* currency — Filling. The pill fills with comb from the bottom as the balance
   rises, and the comb is capped with a wax line at the top of the fill. */
html[data-on-api-filling] .coin-total,
html[data-on-api-filling] .coin-clicker { position: relative; overflow: hidden; }
html[data-on-api-filling] .coin-total::before,
html[data-on-api-filling] .coin-clicker::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: var(--coin-fill, 55%); pointer-events: none;
  background:
    linear-gradient(0deg, rgba(255, 194, 122, 0.2) 0 calc(100% - 1.5px), rgba(255, 222, 190, 0.6) calc(100% - 1.5px)),
    repeating-linear-gradient(60deg,  rgba(242, 115, 155, 0.28) 0 0.5px, transparent 0.5px 7px),
    repeating-linear-gradient(120deg, rgba(242, 115, 155, 0.28) 0 0.5px, transparent 0.5px 7px);
  transition: height 0.6s cubic-bezier(.2, .8, .3, 1);
}

/* toggle — Capped. ON TICK: a wax lid closes over the cell, from the edges in.
   Closing, not popping. */
html[data-on-api-capped] .blk-tick { position: relative; overflow: hidden; }
html[data-on-api-capped] .blk-step.done .blk-tick::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(closest-side, rgba(255, 222, 190, 0.75), rgba(255, 194, 122, 0.45) 70%);
  animation: api-capped-seal 0.5s cubic-bezier(.3, .8, .3, 1) forwards;
}
@keyframes api-capped-seal {
  0%   { clip-path: inset(0 50% 0 50%); opacity: 0; }
  30%  { opacity: 0.95; }
  100% { clip-path: inset(0); opacity: 0; }
}

/* ============ 9 · MYCOLOGIST ============ */

/* card — Rot. A soft irregular bloom spreading from ONE corner, chosen per card by
   nth-child so a grid does not rot in unison. Behind the text; the guard on
   :not(.fx-layer) is why the children rule is written the way it is. */
html[data-on-myco-rot] .record-card,
html[data-on-myco-rot] .loot-card,
html[data-on-myco-rot] .quest-card,
html[data-on-myco-rot] .shared-card,
html[data-on-myco-rot] .codex-card { position: relative; overflow: hidden; }
html[data-on-myco-rot] .record-card > *:not(.fx-layer),
html[data-on-myco-rot] .loot-card > *:not(.fx-layer),
html[data-on-myco-rot] .quest-card > *:not(.fx-layer),
html[data-on-myco-rot] .shared-card > *:not(.fx-layer),
html[data-on-myco-rot] .codex-card > *:not(.fx-layer) { position: relative; z-index: 1; }
html[data-on-myco-rot] .record-card::before,
html[data-on-myco-rot] .loot-card::before,
html[data-on-myco-rot] .quest-card::before,
html[data-on-myco-rot] .shared-card::before,
html[data-on-myco-rot] .codex-card::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(closest-side at 100% 100%, rgba(255, 111, 97, 0.13), transparent 62%),
    radial-gradient(closest-side at 88% 78%, rgba(192, 132, 232, 0.11), transparent 54%);
  animation: myco-rot-spread 14s ease-in-out infinite alternate;
}
@keyframes myco-rot-spread {
  from { transform: scale(0.86); opacity: 0.55; }
  to   { transform: scale(1.14); opacity: 1; }
}
html[data-on-myco-rot] .record-card:nth-child(3n+2)::before { animation-delay: -4.6s; transform-origin: 0 100%; }
html[data-on-myco-rot] .record-card:nth-child(3n+3)::before { animation-delay: -9.2s; transform-origin: 100% 0; }

/* sidebar — Threads. Mycelium creeps BETWEEN the nav items — a fine web in the
   gaps, thickening and thinning as if feeding. */
html[data-on-myco-threads] .sidebar { position: relative; overflow: hidden; }
html[data-on-myco-threads] .sidebar::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    repeating-linear-gradient(38deg, transparent 0 21px, rgba(192, 132, 232, 0.32) 21px 21.5px, transparent 21.5px 47px),
    repeating-linear-gradient(-44deg, transparent 0 26px, rgba(255, 111, 97, 0.26) 26px 26.5px, transparent 26.5px 56px);
  animation: myco-threads-feed 19s ease-in-out infinite alternate;
}
@keyframes myco-threads-feed {
  from { opacity: 0.35; background-size: 100% 100%, 100% 100%; }
  to   { opacity: 0.85; background-size: 118% 108%, 92% 112%; }
}

/* cardState — Blight. ONLY uncollected cards: a dark patch SPREADS across the card
   and pulls back, never quite covering it. The other "needed" treatments in the
   app ask for attention; this one threatens. */
html[data-on-myco-blight] .record-card.is-need { position: relative; overflow: hidden; }
html[data-on-myco-blight] .record-card.is-need::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(closest-side at 12% 106%, rgba(20, 8, 16, 0.85), rgba(140, 63, 95, 0.35) 54%, transparent 78%);
  animation: myco-blight-creep 5.4s cubic-bezier(.4, .05, .5, .95) infinite alternate;
}
@keyframes myco-blight-creep {
  from { transform: scale(0.7); opacity: 0.5; }
  to   { transform: scale(1.35); opacity: 0.95; }
}

/* avatar — Spore Halo. A halo that THINS AND THICKENS rather than pulsing in
   brightness — the ring's own width changes, which is a different read from
   rg-pulse's scale. */
html[data-on-myco-halo] .account-avatar,
html[data-on-myco-halo] .profile-avatar { position: relative; }
html[data-on-myco-halo] .account-avatar::after,
html[data-on-myco-halo] .profile-avatar::after {
  content: ""; position: absolute; inset: -7px; border-radius: 50%; pointer-events: none;
  background: rgba(255, 111, 97, 0.4);
  animation: myco-halo-breathe 5.2s ease-in-out infinite alternate;
}
@keyframes myco-halo-breathe {
  from { -webkit-mask: radial-gradient(closest-side, transparent 78%, #000 82%, transparent 88%);
                 mask: radial-gradient(closest-side, transparent 78%, #000 82%, transparent 88%); opacity: 0.5; }
  to   { -webkit-mask: radial-gradient(closest-side, transparent 62%, #000 72%, transparent 100%);
                 mask: radial-gradient(closest-side, transparent 62%, #000 72%, transparent 100%); opacity: 1; }
}

/* ============ 10 · ALCHEMIST ============ */

/* currency — Titration. The pill carries a graduated scale and the balance reads
   as a LEVEL against it. Static graduations, moving level. */
html[data-on-alch-titrate] .coin-total,
html[data-on-alch-titrate] .coin-clicker { position: relative; overflow: hidden; }
html[data-on-alch-titrate] .coin-total::after,
html[data-on-alch-titrate] .coin-clicker::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 100%; pointer-events: none;
  background-image:
    repeating-linear-gradient(0deg, rgba(212, 216, 244, 0.4) 0 1px, transparent 1px 4px),
    linear-gradient(0deg, rgba(103, 224, 168, 0.22) 0 var(--coin-fill, 48%), transparent var(--coin-fill, 48%));
  background-size: 5px 100%, 100% 100%;
  background-position: 100% 0, 0 0;
  background-repeat: repeat-y, no-repeat;
  transition: background-image 0.5s;
}

/* progress — Reaction. Two liquids meeting: the fill is one colour up to the
   boundary and another beyond it, with a bright reaction line where they touch
   that CREEPS forward. The boundary is the effect. */
html[data-on-alch-react] .xp-bar,
html[data-on-alch-react] .quest-bar { position: relative; overflow: hidden; }
html[data-on-alch-react] .xp-bar::after,
html[data-on-alch-react] .quest-bar::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
  background: linear-gradient(90deg,
    rgba(123, 139, 245, 0.28) 0 calc(var(--fill, 62%) - 4px),
    rgba(230, 255, 245, 0.85) var(--fill, 62%),
    rgba(103, 224, 168, 0.2) calc(var(--fill, 62%) + 5px), transparent);
  animation: alch-react-front 2.4s ease-in-out infinite alternate;
}
@keyframes alch-react-front {
  from { transform: translateX(-2px); filter: none; }
  to   { transform: translateX(3px); }
}

/* toggle — Precipitate. ON TICK: solid falls out of solution and SETTLES at the
   bottom of the box. Ends at rest, unlike every pop in the app. */
html[data-on-alch-precip] .blk-tick { position: relative; overflow: hidden; }
html[data-on-alch-precip] .blk-step.done .blk-tick::after {
  content: ""; position: absolute; left: 1px; right: 1px; height: 3px; pointer-events: none;
  border-radius: 2px; background: rgba(103, 224, 168, 0.85);
  animation: alch-precip-settle 0.8s cubic-bezier(.5, .02, .6, 1) forwards;
}
@keyframes alch-precip-settle {
  0%   { top: 0; opacity: 0; transform: scaleX(0.3); }
  20%  { opacity: 1; }
  78%  { top: calc(100% - 4px); transform: scaleX(1); }
  86%  { top: calc(100% - 5px); }
  100% { top: calc(100% - 4px); opacity: 0.7; }
}

/* card — Decant. The card has a FLUID LEVEL at its foot that tilts a fraction of
   a degree, so a page of them looks like a shelf of part-full jars. */
html[data-on-alch-decant] .record-card,
html[data-on-alch-decant] .loot-card,
html[data-on-alch-decant] .codex-card { position: relative; overflow: hidden; }
html[data-on-alch-decant] .record-card::after,
html[data-on-alch-decant] .loot-card::after,
html[data-on-alch-decant] .codex-card::after {
  content: ""; position: absolute; left: -10%; right: -10%; bottom: 0; height: 34%;
  pointer-events: none; transform-origin: 50% 100%;
  background: linear-gradient(0deg, rgba(123, 139, 245, 0.16) 0 calc(100% - 1px), rgba(212, 216, 244, 0.4) calc(100% - 1px));
  animation: alch-decant-tilt 8.5s ease-in-out infinite alternate;
}
@keyframes alch-decant-tilt {
  from { transform: rotate(-0.7deg); }
  to   { transform: rotate(0.7deg); }
}
html[data-on-alch-decant] .record-card:nth-child(2n)::after { animation-delay: -3.1s; height: 26%; }
html[data-on-alch-decant] .record-card:nth-child(3n)::after { animation-delay: -5.9s; height: 42%; }

/* ============ 11 · GLASSWORKER ============ */

/* title — Frosted. The letters are acid-etched: a fine stipple INSIDE the glyphs
   with the light coming from one side. Static — etching does not move. */
html[data-on-glass-frosted] .page-header h1 {
  background-image:
    repeating-radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.5) 0 0.5px, transparent 0.5px 2.5px),
    linear-gradient(112deg, rgba(214, 244, 252, 0.95), rgba(99, 220, 237, 0.85) 46%, rgba(120, 160, 175, 0.9));
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}

/* chip — Cabochon. The pill is domed: a single hard highlight up top and a shadow
   under it, so a row of tags reads as polished stones. */
html[data-on-glass-cabochon] .chip,
html[data-on-glass-cabochon] .loot-chip {
  position: relative;
  box-shadow: inset 0 -3px 5px -3px rgba(0, 0, 0, 0.55);
}
html[data-on-glass-cabochon] .chip::before,
html[data-on-glass-cabochon] .loot-chip::before {
  content: ""; position: absolute; left: 14%; right: 42%; top: 12%; height: 30%;
  pointer-events: none; border-radius: 50%;
  background: linear-gradient(160deg, rgba(255, 255, 255, 0.55), transparent 72%);
}

/* avatar — Refraction. The badge splits into three chromatic copies at its EDGES
   only, drifting apart and back — the fringing a lens actually produces. Two
   overlays, no filter, nothing on .bdg. */
html[data-on-glass-refract] .account-avatar,
html[data-on-glass-refract] .profile-avatar { position: relative; }
html[data-on-glass-refract] .account-avatar::before,
html[data-on-glass-refract] .profile-avatar::before,
html[data-on-glass-refract] .account-avatar::after,
html[data-on-glass-refract] .profile-avatar::after {
  content: ""; position: absolute; inset: -1px; border-radius: 50%; pointer-events: none;
  -webkit-mask: radial-gradient(closest-side, transparent 76%, #000 86%);
          mask: radial-gradient(closest-side, transparent 76%, #000 86%);
  animation: glass-refract 4.8s ease-in-out infinite alternate;
}
html[data-on-glass-refract] .account-avatar::before,
html[data-on-glass-refract] .profile-avatar::before { background: rgba(120, 190, 255, 0.65); }
html[data-on-glass-refract] .account-avatar::after,
html[data-on-glass-refract] .profile-avatar::after {
  background: rgba(255, 140, 190, 0.55); animation-direction: alternate-reverse;
}
@keyframes glass-refract {
  from { transform: translate3d(-1.2px, 0.6px, 0); opacity: 0.5; }
  to   { transform: translate3d(1.6px, -0.8px, 0); opacity: 0.9; }
}

/* currency — Loupe. The glyph becomes a ground lens, and what it magnifies is the
   digit beside it: the NUMBER scales up a fraction, permanently. The only
   currency effect in the app that touches the value rather than the pill. */
html[data-on-glass-loupe] .coin-total,
html[data-on-glass-loupe] .coin-clicker { position: relative; }
html[data-on-glass-loupe] .coin-total i,
html[data-on-glass-loupe] .coin-clicker i {
  position: relative; border-radius: 50%;
  box-shadow: inset 0 0 0 1.5px rgba(214, 244, 252, 0.75), 0 0 7px rgba(99, 220, 237, 0.35);
}
html[data-on-glass-loupe] .coin-total i::after,
html[data-on-glass-loupe] .coin-clicker i::after {
  content: ""; position: absolute; inset: 1px; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle at 36% 30%, rgba(255, 255, 255, 0.6) 0 12%, transparent 40%);
}
html[data-on-glass-loupe] .coin-total span,
html[data-on-glass-loupe] .coin-clicker span {
  display: inline-block; transform: scale(1.14); transform-origin: 0 50%;
  letter-spacing: 0.01em;
}

/* ============ 12 · FORGEWRIGHT ============ */

/* progress — Billet. The fill is heated steel: WHITE at the leading edge, cooling
   back through orange to dark behind it. The gradient is anchored to the fill's
   end, so the heat is where the work is. */
html[data-on-forge-billet] .xp-fill,
html[data-on-forge-billet] .quest-bar-fill {
  background-image: linear-gradient(90deg,
    rgba(90, 30, 16, 0.95) 0 18%, rgba(196, 62, 26, 0.95) 42%,
    rgba(255, 138, 60, 0.95) 74%, rgba(255, 246, 224, 0.95) 97%) !important;
  box-shadow: 0 0 10px -1px rgba(255, 138, 60, 0.55);
  position: relative; overflow: hidden;
}
html[data-on-forge-billet] .xp-fill::after,
html[data-on-forge-billet] .quest-bar-fill::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(90deg, transparent 60%, rgba(255, 246, 224, 0.35) 92%, transparent);
  animation: forge-billet-heat 2.1s ease-in-out infinite alternate;
}
@keyframes forge-billet-heat { from { opacity: 0.35; } to { opacity: 1; } }

/* toggle — Rivet. ON TICK: the box is PUNCHED — it compresses flat for two frames,
   then a set rivet head is left in it. Hard, short, metallic. */
html[data-on-forge-rivet] .blk-step.done .blk-tick {
  animation: forge-rivet-punch 0.22s steps(3, end);
}
@keyframes forge-rivet-punch {
  0%   { transform: scale(1.1, 0.72); }
  50%  { transform: scale(0.94, 1.06); }
  100% { transform: scale(1, 1); }
}
html[data-on-forge-rivet] .blk-step.done .blk-tick::after {
  content: ""; position: absolute; left: 50%; top: 50%; width: 6px; height: 6px;
  margin: -3px 0 0 -3px; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle at 36% 32%, rgba(255, 240, 210, 0.9) 0 22%, rgba(150, 90, 50, 0.9) 70%);
  box-shadow: 0 0 0 0.5px rgba(90, 50, 26, 0.8);
}

/* nav — Tang. The current item is held by a forged tang: a tapered wedge at its
   left edge, glowing where it enters. */
html[data-on-forge-tang] .nav-item.active { position: relative; }
html[data-on-forge-tang] .nav-item.active::before {
  content: ""; position: absolute; left: 0; top: 22%; bottom: 22%; width: 7px;
  pointer-events: none;
  clip-path: polygon(0 0, 100% 26%, 100% 74%, 0 100%);
  background: linear-gradient(90deg, rgba(255, 246, 224, 0.9), rgba(255, 106, 43, 0.85) 60%, rgba(120, 40, 18, 0.9));
  animation: forge-tang-glow 3.1s ease-in-out infinite alternate;
}
@keyframes forge-tang-glow { from { opacity: 0.6; } to { opacity: 1; } }

/* currency — Struck. The pill's rim shows hammer facets — uneven, because a struck
   edge is not a machined one. Static. */
html[data-on-forge-struck] .coin-total,
html[data-on-forge-struck] .coin-clicker {
  border-radius: 4px;
  clip-path: polygon(
    0 22%, 5px 4px, 22% 0, 54% 3px, 78% 0, calc(100% - 4px) 5px, 100% 26%,
    100% 74%, calc(100% - 5px) calc(100% - 3px), 74% 100%, 46% calc(100% - 3px),
    20% 100%, 4px calc(100% - 4px), 0 76%);
  box-shadow: inset 0 0 0 1px rgba(255, 138, 60, 0.35);
}

/* ============ 13 · WEAVER ============ */

/* title — Brocade. The letters carry a woven texture — warp and weft inside the
   glyphs, not a gradient across them. Static. */
html[data-on-weave-brocade] .page-header h1 {
  background-image:
    repeating-linear-gradient(0deg,  rgba(0, 0, 0, 0.35) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.3) 0 1px, transparent 1px 3px),
    linear-gradient(104deg, rgba(226, 99, 189, 0.95), rgba(245, 143, 212, 0.95) 50%, rgba(150, 47, 119, 0.95));
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}

/* sidebar — Selvedge. The rail's edge is a finished selvedge with a short fringe
   hanging off it. Static: a finished edge is finished. */
html[data-on-weave-selvedge] .sidebar { position: relative; }
html[data-on-weave-selvedge] .sidebar::after {
  content: ""; position: absolute; top: 0; bottom: 0; right: -5px; width: 11px;
  pointer-events: none;
  background-image:
    linear-gradient(90deg, rgba(226, 99, 189, 0.5) 0 2px, transparent 2px),
    repeating-linear-gradient(0deg, rgba(245, 143, 212, 0.4) 0 1.5px, transparent 1.5px 6px);
  background-size: 100% 100%, 100% 100%;
  -webkit-mask: linear-gradient(90deg, #000 0 3px, rgba(0, 0, 0, 0.5) 3px 7px, transparent 10px);
          mask: linear-gradient(90deg, #000 0 3px, rgba(0, 0, 0, 0.5) 3px 7px, transparent 10px);
}

/* chip — Ribbon. The tag has a ribbon's cut tail at its trailing end. Static. */
html[data-on-weave-ribbon] .chip,
html[data-on-weave-ribbon] .loot-chip {
  border-radius: 3px 0 0 3px; padding-right: 14px;
  clip-path: polygon(0 0, 100% 0, calc(100% - 7px) 50%, 100% 100%, 0 100%);
}

/* cardState — Basted. ONLY uncollected cards: a running stitch round the border
   that ADVANCES a stitch at a time. Marching ants crawl smoothly; a needle does
   not — steps() is the whole difference. */
html[data-on-weave-basted] .record-card.is-need {
  border-color: transparent !important;
  background-image:
    repeating-linear-gradient(90deg, rgba(245, 143, 212, 0.8) 0 5px, transparent 5px 11px),
    repeating-linear-gradient(90deg, rgba(245, 143, 212, 0.8) 0 5px, transparent 5px 11px),
    repeating-linear-gradient(0deg, rgba(226, 99, 189, 0.7) 0 5px, transparent 5px 11px),
    repeating-linear-gradient(0deg, rgba(226, 99, 189, 0.7) 0 5px, transparent 5px 11px);
  background-size: 11px 1.5px, 11px 1.5px, 1.5px 11px, 1.5px 11px;
  background-position: 0 0, 0 100%, 0 0, 100% 0;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  animation: weave-basted-stitch 1.4s steps(4, end) infinite;
}
@keyframes weave-basted-stitch {
  to { background-position: 11px 0, -11px 100%, 0 -11px, 100% 11px; }
}

/* ============ 14 · COOK ============ */

/* toggle — Plated. ON TICK: a plate is SET DOWN — it arrives from above, lands, and
   settles once. */
html[data-on-cook-plated] .blk-step.done .blk-tick {
  animation: cook-plated-set 0.42s cubic-bezier(.4, .02, .3, 1.4);
}
@keyframes cook-plated-set {
  0%   { transform: translateY(-7px) scale(1.1); }
  60%  { transform: translateY(1px) scale(0.97); }
  100% { transform: translateY(0) scale(1); }
}
html[data-on-cook-plated] .blk-step.done .blk-tick::after {
  content: ""; position: absolute; inset: 2px; border-radius: 50%; pointer-events: none;
  border: 1px solid rgba(240, 212, 136, 0.75);
}

/* card — Menu. A dotted leader runs from the card's title to its value, the way a
   menu line does. Static, and it is typography rather than decoration. */
html[data-on-cook-menu] .record-card,
html[data-on-cook-menu] .loot-card { position: relative; }
html[data-on-cook-menu] .record-card::after,
html[data-on-cook-menu] .loot-card::after {
  content: ""; position: absolute; left: 12px; right: 12px; top: 15px; height: 1px;
  pointer-events: none; opacity: 0.5;
  background: repeating-linear-gradient(90deg, rgba(232, 244, 206, 0.7) 0 1.5px, transparent 1.5px 5px);
}

/* nav — Mise. The current item is marked with a knife edge: a fine bright line and
   a bevel behind it, angled. Static. */
html[data-on-cook-mise] .nav-item.active { position: relative; }
html[data-on-cook-mise] .nav-item.active::before {
  content: ""; position: absolute; left: 1px; top: 18%; bottom: 18%; width: 4px;
  pointer-events: none;
  clip-path: polygon(0 0, 100% 12%, 100% 88%, 0 100%);
  background: linear-gradient(90deg, rgba(240, 246, 232, 0.95), rgba(158, 207, 66, 0.5));
}

/* progress — Reduction. The UNFILLED part evaporates: steam rises off the gap and
   the gap's own tone thins as it goes. The bar reads as something cooking down. */
html[data-on-cook-reduce] .xp-bar,
html[data-on-cook-reduce] .quest-bar { position: relative; overflow: visible; }
html[data-on-cook-reduce] .xp-bar::after,
html[data-on-cook-reduce] .quest-bar::after {
  content: ""; position: absolute; left: var(--fill, 62%); right: 0; bottom: 100%; height: 11px;
  pointer-events: none;
  background:
    radial-gradient(closest-side at 20% 100%, rgba(240, 236, 214, 0.4), transparent 70%),
    radial-gradient(closest-side at 58% 100%, rgba(240, 236, 214, 0.34), transparent 70%),
    radial-gradient(closest-side at 86% 100%, rgba(240, 236, 214, 0.38), transparent 70%);
  animation: cook-reduce-steam 3.4s ease-in-out infinite;
}
@keyframes cook-reduce-steam {
  0%   { transform: translateY(4px) scaleY(0.5); opacity: 0; }
  35%  { opacity: 0.85; }
  100% { transform: translateY(-9px) scaleY(1.5); opacity: 0; }
}

/* ============ 15 · SCRIBE ============ */

/* title — Versal. The heading's FIRST LETTER is a drawn capital: larger, gilded,
   and set on a ruled block. ::first-letter, so no markup changes and no JS. */
html[data-on-scribe-versal] .page-header h1::first-letter {
  font-size: 1.5em;
  line-height: 0.85;
  padding: 0 0.06em 0 0.02em;
  background-image:
    repeating-linear-gradient(0deg, rgba(139, 164, 201, 0.35) 0 1px, transparent 1px 4px),
    linear-gradient(122deg, rgba(255, 243, 196, 0.98), rgba(221, 196, 140, 0.98) 44%, rgba(140, 106, 36, 0.95));
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}

/* nav — Manicule. The current item is marked by a drawn bracket pointing at it,
   the way a reader marks a passage. Static; a pointing hand does not wave. */
html[data-on-scribe-manicule] .nav-item.active { position: relative; }
html[data-on-scribe-manicule] .nav-item.active::before {
  content: ""; position: absolute; left: 1px; top: 50%; width: 6px; height: 11px;
  margin-top: -5.5px; pointer-events: none; opacity: 0.85;
  border-left: 1.5px solid rgba(221, 196, 140, 0.9);
  border-top: 1.5px solid rgba(221, 196, 140, 0.9);
  border-bottom: 1.5px solid rgba(221, 196, 140, 0.9);
  border-radius: 3px 0 0 3px;
}
html[data-on-scribe-manicule] .nav-item.active::after {
  content: ""; position: absolute; left: 7px; top: 50%; width: 5px; height: 1.5px;
  margin-top: -0.75px; pointer-events: none;
  background: rgba(221, 196, 140, 0.9);
}

/* sidebar — Margin. The rail is a ruled page: feint lines across it and a single
   ruled margin down its edge. The quietest sidebar in the app, deliberately —
   this element's cosmetic is restraint. */
html[data-on-scribe-margin] .sidebar {
  background-image:
    repeating-linear-gradient(0deg, transparent 0 21px, rgba(139, 164, 201, 0.13) 21px 22px, transparent 22px),
    linear-gradient(90deg, transparent 0 13px, rgba(221, 196, 140, 0.22) 13px 14px, transparent 14px);
}

/* avatar — Seal. The badge is stamped in wax: an irregular pressed rim with the
   spread a seal actually makes. Static, on an overlay, nothing on .bdg. */
html[data-on-scribe-seal] .account-avatar,
html[data-on-scribe-seal] .profile-avatar { position: relative; }
html[data-on-scribe-seal] .account-avatar::before,
html[data-on-scribe-seal] .profile-avatar::before {
  content: ""; position: absolute; inset: -4px; pointer-events: none;
  border-radius: 48% 52% 51% 49% / 52% 48% 52% 48%;
  background: rgba(160, 60, 52, 0.5);
  box-shadow: inset 0 -2px 4px -1px rgba(0, 0, 0, 0.6), 0 1px 3px rgba(0, 0, 0, 0.5);
  -webkit-mask: radial-gradient(closest-side, transparent 72%, #000 78%);
          mask: radial-gradient(closest-side, transparent 72%, #000 78%);
}


/* ===========================================================================
   PASS 4 of 4: THE FEED — worn by a POST, not by the page.

   TWO PER PROFESSION, thirty in all, six to each of the five feed slots. Two
   rather than four because of the constraint effects.css already states for this
   half of the catalog: a feed draws many cards at once, so nothing here may
   spawn particles per card beyond the existing budget. Every engine below is a
   gradient, a border, a mask or a transition — no particle hosts, no filters.

   THE BOUNDARY, restated because it is the one rule here that is not aesthetic:
   these decorate what OTHER PEOPLE see when they read something you wrote. They
   hang off `.post[data-fx-<id>]` — an attribute on the CARD — never off <html>.
   Two things make it hold, both structural:
     1. the attributes are only ever written onto .post, so no html-scoped rule
        can be triggered by a post;
     2. every selector below is prefixed with .post, so even a leaked attribute
        would match nothing.
   A rule in this section that starts with `html` is a bug. A post must not be
   able to give the reader an aurora.

   AND ON BADGES: nothing here may create a stacking context on `.bdg` — no
   transform, filter, opacity or z-index on the badge itself. Ring-type work goes
   on the wrapper, `.post-head > [data-card-user]`, which is the trap that eats
   rank rings and the reason Gold Standard is written that way.

   Layering inside a card, from the table at the top of effects.css: the wash is
   band 0, content is band 1, edge decoration is 2-9.
   =========================================================================== */

/* ---------------- postFrame — six ---------------- */

/* PROSPECTOR — Assayed. The card's edge is BEVELLED, not bordered: the corners are
   cut and the cut catches light on two sides only, so the post reads as a stone
   that was faced. Static. */
.post[data-fx-frame-assayed] {
  border-color: transparent;
  clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
  box-shadow: inset 1px 1px 0 rgba(255, 232, 180, 0.28), inset -1px -1px 0 rgba(0, 0, 0, 0.5);
}

/* WOODWRIGHT — Mitred. The four edges meet at MITRES — a corner joint drawn at each
   corner, and nothing along the runs. A frame made rather than a frame lit. */
.post[data-fx-frame-mitred] { border-color: rgba(201, 138, 69, 0.4); }
.post[data-fx-frame-mitred]::before,
.post[data-fx-frame-mitred]::after {
  content: ""; position: absolute; width: 18px; height: 18px; pointer-events: none; z-index: 2;
  border: 1.5px solid rgba(201, 138, 69, 0.8);
}
.post[data-fx-frame-mitred]::before {
  left: -1px; top: -1px; border-right: 0; border-bottom: 0; border-radius: inherit 0 0 0;
  background: linear-gradient(135deg, rgba(201, 138, 69, 0.18), transparent 60%);
}
.post[data-fx-frame-mitred]::after {
  right: -1px; bottom: -1px; border-left: 0; border-top: 0;
  background: linear-gradient(-45deg, rgba(201, 138, 69, 0.18), transparent 60%);
}

/* FORGEWRIGHT — Welded. A seam runs the whole edge with the bead visible in it, and
   the bead's HEAT travels round once every eight seconds. The only frame here that
   moves along its own length. */
.post[data-fx-frame-welded] { border-color: rgba(120, 40, 18, 0.7); }
.post[data-fx-frame-welded]::before {
  content: ""; position: absolute; inset: -1px; border-radius: inherit; padding: 2px;
  pointer-events: none; z-index: 2;
  background:
    conic-gradient(from var(--fx-ang, 0deg),
      rgba(90, 30, 16, 0.9) 0deg 250deg, rgba(255, 138, 60, 0.9) 320deg,
      rgba(255, 246, 224, 0.95) 344deg, rgba(90, 30, 16, 0.9) 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  animation: post-welded 8s linear infinite;
}
@keyframes post-welded { to { --fx-ang: 360deg; } }
/* The bead itself — a beaded texture over the seam, static, so the travelling heat
   has something to travel along. */
.post[data-fx-frame-welded]::after {
  content: ""; position: absolute; inset: -1px; border-radius: inherit; padding: 2px;
  pointer-events: none; z-index: 2; opacity: 0.5;
  background: repeating-linear-gradient(90deg, rgba(255, 214, 150, 0.5) 0 2px, transparent 2px 5px);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
}

/* WEAVER — Bound. The edge is a woven BINDING — a tape wrapped round the card with
   its weave visible, thicker at top and bottom the way a bound edge is. Static. */
.post[data-fx-frame-bound] {
  border: 3px solid transparent;
  background-image:
    linear-gradient(var(--surface-1), var(--surface-1)),
    repeating-linear-gradient(90deg, rgba(226, 99, 189, 0.55) 0 2px, rgba(150, 47, 119, 0.5) 2px 4px);
  background-origin: border-box;
  background-clip: padding-box, border-box;
}

/* APIARIST — Sealed. The card is capped: its edge is a wax fillet, thickest at the
   corners, with a soft inner spread. Static. */
.post[data-fx-frame-sealed] {
  border-color: rgba(255, 222, 190, 0.35);
  box-shadow:
    inset 0 0 0 2px rgba(255, 194, 122, 0.16),
    inset 0 0 11px -3px rgba(255, 194, 122, 0.4);
}
.post[data-fx-frame-sealed]::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: 2;
  background:
    radial-gradient(closest-side at 0 0, rgba(255, 194, 122, 0.3), transparent 26%),
    radial-gradient(closest-side at 100% 0, rgba(255, 194, 122, 0.3), transparent 26%),
    radial-gradient(closest-side at 0 100%, rgba(255, 194, 122, 0.28), transparent 26%),
    radial-gradient(closest-side at 100% 100%, rgba(255, 194, 122, 0.28), transparent 26%);
}

/* HOMESTEADER — Posted. The card is fenced: posts down both sides with two rails
   between them, and nothing across the top or bottom. Static, and the asymmetry is
   the read — a fence has sides. */
.post[data-fx-frame-posted] { border-color: transparent; }
.post[data-fx-frame-posted]::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; z-index: 2;
  background-image:
    linear-gradient(90deg, rgba(106, 167, 156, 0.7) 0 2px, transparent 2px),
    linear-gradient(-90deg, rgba(106, 167, 156, 0.7) 0 2px, transparent 2px),
    linear-gradient(0deg, transparent 30%, rgba(106, 167, 156, 0.4) 30% 31.5%, transparent 31.5% 62%, rgba(106, 167, 156, 0.4) 62% 63.5%, transparent 63.5%);
}

/* ---------------- postBack — six ---------------- */
/* The wash element is spawned by effects-run.js, as it is for every other feed
   backdrop. These style it; they do not fill it with particles. */

/* HERBALIST — Hedgerow. A dense band of leaf shapes along the card's foot, behind
   the words. Static — a hedge does not move. */
.post[data-fx-back-hedgerow] > .fx-post-wash {
  background:
    radial-gradient(ellipse 22px 9px at 12% 100%, rgba(116, 207, 138, 0.22) 0 62%, transparent 66%),
    radial-gradient(ellipse 15px 13px at 34% 100%, rgba(116, 207, 138, 0.18) 0 62%, transparent 66%),
    radial-gradient(ellipse 26px 8px at 58% 100%, rgba(158, 224, 138, 0.16) 0 62%, transparent 66%),
    radial-gradient(ellipse 13px 15px at 79% 100%, rgba(116, 207, 138, 0.2) 0 62%, transparent 66%),
    linear-gradient(0deg, rgba(116, 207, 138, 0.1), transparent 40%);
}

/* MYCOLOGIST — Spotted. Irregular blooms at two corners that BREATHE out of step
   with each other, so the card never looks symmetrical. */
.post[data-fx-back-spotted] > .fx-post-wash {
  background:
    radial-gradient(closest-side at 96% 8%, rgba(255, 111, 97, 0.16), transparent 60%),
    radial-gradient(closest-side at 6% 94%, rgba(192, 132, 232, 0.14), transparent 56%);
  animation: post-spotted 11s ease-in-out infinite alternate;
}
@keyframes post-spotted {
  from { background-size: 70% 70%, 100% 100%; opacity: 0.6; }
  to   { background-size: 110% 100%, 76% 82%; opacity: 1; }
}

/* TIDECALLER — Shallows. A depth gradient with the light coming from the TOP of the
   card, and a still waterline across it. Static. */
.post[data-fx-back-shallows] > .fx-post-wash {
  background:
    linear-gradient(180deg, rgba(127, 227, 224, 0.14), transparent 42%),
    linear-gradient(0deg, rgba(30, 95, 143, 0.2), transparent 58%),
    linear-gradient(0deg, transparent 33%, rgba(215, 248, 252, 0.22) 33% 34%, transparent 34%);
}

/* ALCHEMIST — Suspension. Fine sediment held in the card, densest at the foot, with
   a slow SETTLE that never completes. */
.post[data-fx-back-suspension] > .fx-post-wash {
  background:
    repeating-radial-gradient(circle at 30% 40%, rgba(212, 216, 244, 0.16) 0 1px, transparent 1px 9px),
    repeating-radial-gradient(circle at 72% 66%, rgba(103, 224, 168, 0.13) 0 1px, transparent 1px 12px),
    linear-gradient(0deg, rgba(123, 139, 245, 0.16), transparent 62%);
  background-size: 60px 46px, 84px 62px, 100% 100%;
  animation: post-suspension 17s linear infinite;
}
@keyframes post-suspension {
  to { background-position: 0 46px, 0 62px, 0 0; }
}

/* COOK — Scorched. A single warm mark at the card's foot, as if it had been set down
   on something hot. Off-centre and static; a scorch is a record. */
.post[data-fx-back-scorched] > .fx-post-wash {
  background:
    radial-gradient(closest-side at 34% 108%, rgba(240, 212, 136, 0.2), rgba(120, 60, 20, 0.12) 52%, transparent 74%),
    linear-gradient(0deg, rgba(158, 207, 66, 0.08), transparent 44%);
}

/* BONE-HUNTER — Interred. Cold, and the light falls from ONE side only, hard. The
   only feed backdrop with no warm value in it at all. Static. */
.post[data-fx-back-interred] > .fx-post-wash {
  background:
    linear-gradient(96deg, rgba(206, 206, 216, 0.11) 0 22%, transparent 46%),
    linear-gradient(276deg, rgba(0, 0, 0, 0.34) 0 30%, transparent 58%),
    repeating-linear-gradient(90deg, transparent 0 15px, rgba(206, 206, 216, 0.055) 15px 17px, transparent 17px 38px);
}

/* ---------------- postName — six ---------------- */

/* SCRIBE — Engrossed. The name is written in a formal hand: letter-spaced, with a
   fine rule under it that stops short of the last glyph. Static. */
.post[data-fx-name-engrossed] .post-name {
  letter-spacing: 0.05em;
  color: rgba(236, 233, 226, 0.96);
  position: relative;
}
.post[data-fx-name-engrossed] .post-name::after {
  content: ""; position: absolute; left: 0; right: 12%; bottom: -3px; height: 1px;
  background: linear-gradient(90deg, rgba(221, 196, 140, 0.8), rgba(139, 164, 201, 0.3));
}

/* WEAVER — Sampler. The name is stitched: a cross-stitch texture inside the glyphs.
   Static, and the texture reads at 13px, which is the size it has to work at. */
.post[data-fx-name-sampler] .post-name {
  background-image:
    repeating-linear-gradient(45deg, rgba(0, 0, 0, 0.4) 0 1px, transparent 1px 2.5px),
    repeating-linear-gradient(-45deg, rgba(255, 255, 255, 0.3) 0 1px, transparent 1px 2.5px),
    linear-gradient(100deg, rgba(226, 99, 189, 0.98), rgba(245, 143, 212, 0.98));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

/* GLASSWORKER — Leaded. The name is a leaded light: each glyph outlined dark with
   colour inside it. Achieved with a stroke, so no gradient and no fill loss. */
.post[data-fx-name-leaded] .post-name {
  color: rgba(166, 240, 251, 0.95);
  -webkit-text-stroke: 0.6px rgba(10, 24, 30, 0.9);
  paint-order: stroke fill;
  text-shadow: 0 0 6px rgba(99, 220, 237, 0.3);
}

/* BONE-HUNTER — Incised. The name is cut in rather than written on: a light edge
   above, a dark one below, no colour change. The most restrained name here. */
.post[data-fx-name-incised] .post-name {
  color: rgba(180, 180, 190, 0.95);
  text-shadow: 0 -0.5px 0 rgba(226, 226, 234, 0.5), 0 1px 0 rgba(0, 0, 0, 0.8);
}

/* PROSPECTOR — Struck. The name is stamped into metal: pressed, with the burr the
   punch leaves on one side. Static. */
.post[data-fx-name-struck] .post-name {
  color: rgba(210, 190, 160, 0.9);
  text-shadow:
    0 1px 0 rgba(255, 232, 180, 0.35),
    0 -1px 1px rgba(0, 0, 0, 0.85),
    1px 0 0 rgba(0, 0, 0, 0.4);
}

/* COOK — Chalked. The name is on a board: a chalky edge and a slightly uneven
   weight, as if written by hand. Static. */
.post[data-fx-name-chalked] .post-name {
  color: rgba(243, 247, 230, 0.92);
  text-shadow: 0 0 1px rgba(243, 247, 230, 0.6), 0 0 5px rgba(232, 244, 206, 0.2);
  letter-spacing: 0.015em;
}

/* ---------------- postBadge — six ---------------- */
/* Wrapper only. Nothing below touches .bdg's transform, filter, opacity or
   z-index — see the note at the head of this section. */

/* BEASTMASTER — Collared. A band round the badge with a single ring hanging at the
   bottom of it. Static, and the ring is what makes it a collar rather than a halo. */
.post[data-fx-badge-collared] .post-head > [data-card-user] {
  display: inline-block; position: relative; border-radius: 50%; padding: 2px;
  background: conic-gradient(from 90deg, rgba(176, 122, 240, 0.85) 0deg 340deg, rgba(255, 164, 100, 0.9) 360deg);
}
.post[data-fx-badge-collared] .post-head > [data-card-user]::after {
  content: ""; position: absolute; left: 50%; bottom: -4px; width: 6px; height: 6px;
  margin-left: -3px; border-radius: 50%; pointer-events: none;
  border: 1.5px solid rgba(255, 164, 100, 0.9);
}

/* APIARIST — Combed. A hex ring round the badge, and it turns ONCE on hover rather
   than continuously — the hive notices you and settles again. */
.post[data-fx-badge-combed] .post-head > [data-card-user] {
  display: inline-block; border-radius: 50%; padding: 2.5px;
  background:
    repeating-conic-gradient(from 0deg, rgba(255, 194, 122, 0.9) 0deg 5deg, transparent 5deg 15deg);
  transition: transform 0.6s cubic-bezier(.3, .8, .3, 1);
}
.post[data-fx-badge-combed] .post-head > [data-card-user]:hover { transform: rotate(60deg); }

/* MYCOLOGIST — Fruited. Small caps sprout round the badge on hover — three of them,
   at uneven angles, arriving at slightly different times. */
.post[data-fx-badge-fruited] .post-head > [data-card-user] { display: inline-block; position: relative; }
.post[data-fx-badge-fruited] .post-head > [data-card-user]::before,
.post[data-fx-badge-fruited] .post-head > [data-card-user]::after {
  content: ""; position: absolute; width: 7px; height: 5px; pointer-events: none;
  border-radius: 50% 50% 20% 20%;
  background: rgba(255, 111, 97, 0.85);
  transform: scale(0); transform-origin: 50% 100%;
  transition: transform 0.3s cubic-bezier(.2, 1.5, .4, 1);
}
.post[data-fx-badge-fruited] .post-head > [data-card-user]::before { left: -4px; top: 22%; }
.post[data-fx-badge-fruited] .post-head > [data-card-user]::after  { right: -3px; top: 58%; transition-delay: 0.08s; }
.post[data-fx-badge-fruited] .post-head > [data-card-user]:hover::before,
.post[data-fx-badge-fruited] .post-head > [data-card-user]:hover::after { transform: scale(1); }

/* TIDECALLER — Moored. A rope collar — two twisted strands round the badge, drawn
   as a doubled conic so the twist reads. Static. */
.post[data-fx-badge-moored] .post-head > [data-card-user] {
  display: inline-block; border-radius: 50%; padding: 2.5px;
  background:
    repeating-conic-gradient(from 0deg,
      rgba(226, 214, 168, 0.85) 0deg 6deg, rgba(120, 100, 66, 0.8) 6deg 12deg);
}

/* SCRIBE — Countersigned. A fine double rule round the badge and a small mark at the
   lower right, the way a clerk initials a page. Static, and the quietest badge
   treatment in the feed. */
.post[data-fx-badge-countersigned] .post-head > [data-card-user] {
  display: inline-block; position: relative; border-radius: 50%; padding: 2px;
  background: rgba(139, 164, 201, 0.5);
  box-shadow: inset 0 0 0 0.5px rgba(221, 196, 140, 0.7);
}
.post[data-fx-badge-countersigned] .post-head > [data-card-user]::after {
  content: ""; position: absolute; right: -2px; bottom: -1px; width: 5px; height: 5px;
  pointer-events: none; border-radius: 0 0 4px 0;
  border-right: 1.5px solid rgba(221, 196, 140, 0.9);
  border-bottom: 1.5px solid rgba(221, 196, 140, 0.9);
}

/* GLASSWORKER — Cased. The badge is set in glass: a bright rim on the upper left, a
   dark one lower right, and a fine highlight arc. Static — the light is caught, not
   moving. */
.post[data-fx-badge-cased] .post-head > [data-card-user] {
  display: inline-block; position: relative; border-radius: 50%; padding: 2px;
  background: linear-gradient(142deg, rgba(230, 250, 255, 0.9), rgba(99, 220, 237, 0.4) 46%, rgba(12, 32, 40, 0.9));
}
.post[data-fx-badge-cased] .post-head > [data-card-user]::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle at 34% 26%, rgba(255, 255, 255, 0.7) 0 8%, transparent 26%);
}

/* ---------------- postAct — six ---------------- */

/* FORGEWRIGHT — Struck. The control takes a HAMMER BLOW on press: it flattens for
   two frames and comes back. Steps, not easing — a blow is discrete. */
.post[data-fx-act-struck] .post-act { transition: box-shadow 0.2s; }
.post[data-fx-act-struck] .post-act:hover { box-shadow: inset 0 -2px 6px -2px rgba(255, 138, 60, 0.5); }
.post[data-fx-act-struck] .post-act:active { animation: post-act-struck 0.16s steps(2, end); }
@keyframes post-act-struck {
  0%   { transform: scale(1.06, 0.82); }
  100% { transform: scale(1, 1); }
}

/* ALCHEMIST — Titrated. On hover the control FILLS from the bottom, one measured
   step at a time rather than smoothly. */
.post[data-fx-act-titrated] .post-act { position: relative; overflow: hidden; }
.post[data-fx-act-titrated] .post-act::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 0; pointer-events: none;
  background: linear-gradient(0deg, rgba(103, 224, 168, 0.3), rgba(123, 139, 245, 0.2));
  transition: height 0.4s steps(4, end);
}
.post[data-fx-act-titrated] .post-act:hover::before { height: 100%; }

/* HERBALIST — Grafted. A shoot grows along the control's underside on hover and
   opens one leaf at the tip. Grows from the left; nothing pops. */
.post[data-fx-act-grafted] .post-act { position: relative; }
.post[data-fx-act-grafted] .post-act::after {
  content: ""; position: absolute; left: 0; bottom: 1px; height: 1.5px; width: 100%;
  border-radius: 2px; transform: scaleX(0); transform-origin: left; pointer-events: none;
  background: linear-gradient(90deg, rgba(116, 207, 138, 0.9), rgba(158, 224, 138, 0.5));
  transition: transform 0.42s cubic-bezier(.3, .9, .35, 1);
}
.post[data-fx-act-grafted] .post-act:hover::after { transform: scaleX(1); }
.post[data-fx-act-grafted] .post-act::before {
  content: ""; position: absolute; right: -2px; bottom: 0; width: 5px; height: 4px;
  border-radius: 0 100% 0 100%; pointer-events: none;
  background: rgba(116, 207, 138, 0.9);
  transform: scale(0); transform-origin: 0 100%;
  transition: transform 0.24s cubic-bezier(.2, 1.5, .4, 1) 0.34s;
}
.post[data-fx-act-grafted] .post-act:hover::before { transform: scale(1); }

/* HOMESTEADER — Latched. On press the control CLOSES like a gate latch: it swings a
   degree and stops. The calmest control in the feed, and it ends at rest. */
.post[data-fx-act-latched] .post-act {
  transform-origin: 6% 50%;
  transition: transform 0.24s cubic-bezier(.3, .9, .3, 1), border-color 0.24s;
}
.post[data-fx-act-latched] .post-act:hover { border-color: rgba(106, 167, 156, 0.7); }
.post[data-fx-act-latched] .post-act:active { transform: rotate(1.6deg); }

/* WOODWRIGHT — Chamfered. On hover the control's corners are cut back — the edge is
   taken off, which is what a chamfer is. No colour change. */
.post[data-fx-act-chamfered] .post-act {
  transition: clip-path 0.26s cubic-bezier(.3, .9, .3, 1), box-shadow 0.26s;
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
.post[data-fx-act-chamfered] .post-act:hover {
  clip-path: polygon(6px 0, calc(100% - 6px) 0, 100% 50%, calc(100% - 6px) 100%, 6px 100%, 0 50%);
  box-shadow: inset 0 0 0 1px rgba(201, 138, 69, 0.5);
}

/* BEASTMASTER — Baited. The control leans TOWARD the cursor on hover and snaps back
   on release — it wants the thing. */
.post[data-fx-act-baited] .post-act {
  transition: transform 0.2s cubic-bezier(.2, 1.3, .4, 1), color 0.2s;
}
.post[data-fx-act-baited] .post-act:hover { transform: scale(1.09) translateY(-1px); }
.post[data-fx-act-baited] .post-act:active { transform: scale(0.94) translateY(1px); }

/* ---------------- motion guards for the feed ----------------
   Broad on purpose, as effects.css's own feed block is: everything in this
   section is decoration and naming each id would mean editing this list every
   time the roster grew. */
@media (prefers-reduced-motion: reduce) {
  .post[data-fx-frame-welded]::before,
  .post[data-fx-back-spotted] > .fx-post-wash,
  .post[data-fx-back-suspension] > .fx-post-wash,
  .post[data-fx-act-struck] .post-act,
  .post[data-fx-badge-combed] .post-head > [data-card-user] { animation: none !important; transition: none !important; }
}
html[data-intensity="calm"] .post[data-fx-frame-welded]::before,
html[data-intensity="calm"] .post[data-fx-back-spotted] > .fx-post-wash,
html[data-intensity="calm"] .post[data-fx-back-suspension] > .fx-post-wash { animation: none !important; }


/* ===========================================================================
   PASS 5: ICONOGRAPHY — glyph particles, corner flourishes, border crawl.

   WHY THIS IS A LAYER AND NOT A REWRITE. Passes 1-4 built 120 engines out of
   geometry, and geometry is the right tool for a travel curve — but it is the
   wrong tool for a SUBJECT. effects.css already knew this: `.fx-p svg { display:
   block }` exists so Leaf Litter can drop actual leaves. This pass gives every
   profession the same courtesy, using the icon FONT rather than SVG: one text
   node, no markup to draw, and the glyph inherits `--fx-tint` through
   `.fx-p`'s own `color`.

   THE COMPOSITING TRICK, and the reason this is ~200 lines instead of 2000:
   a glyph particle is still a `.fx-p`, so it inherits its engine's animation
   for free — Forgewright's cinder curve, Herbalist's stalling arc, Apiarist's
   closed orbit. `.fx-g` only cancels the geometric PAINT (background, border,
   box-shadow, fixed width/height) and leaves the motion alone. Layering a glyph
   over an engine costs one class.

   THE ONE PLACE THE FORMULA IS SHARED ON PURPOSE. Corner flourishes and the
   border crawl are a NEW axis, and the user's call is that a shared formula is
   fine here so long as every profession gets one. So there is a single corner
   engine and a single crawl engine, each driven by two per-profession variables
   (--cn-glyph is set in markup; --cn-spin / --crawl-dur here). Passes 1-4 stay
   bespoke; this pass is deliberately systematic, because fifteen bespoke corner
   treatments would be fifteen ways to put a mark in a corner.

   GLYPH VOCABULARY — every name MEASURED against the loaded icon build, not
   inferred from being referenced elsewhere in the app. That distinction is not
   pedantry: `ti-shell` appears in effects.js's CURRENCY_ICONS and is absent from
   the build, so Bone-hunter's whole symbol identity rendered at zero width in
   silence. `assertGlyphs()` in professions-effects.js is the check.

   Two glyphs are shared across two professions each, which the brief allows where
   it makes sense: a curl of wood and a coil of thread are both ti-spiral, and
   forge-fire and kitchen-fire are both fire. They differ in colour, size, motion
   and density.

     Prospector   ti-diamond          · ti-diamond-filled
     Herbalist    ti-leaf             · ti-seeding
     Woodwright   ti-seeding          · ti-spiral
     Tidecaller   ti-droplet-filled   · ti-anchor
     Bone-hunter  ti-bone              · ti-circle-filled
     Beastmaster  ti-moon-stars       · ti-star-filled
     Homesteader  ti-seeding          · ti-circle-filled
     Apiarist     ti-hexagon-filled   · ti-sparkles
     Mycologist   ti-mushroom-filled  · ti-circle-filled
     Alchemist    ti-atom             · ti-droplet-filled
     Glassworker  ti-sparkles         · ti-crystal-ball
     Forgewright  ti-flame            · ti-bolt
     Weaver       ti-spiral           · ti-circle-filled
     Cook         ti-campfire         · ti-flame
     Scribe       ti-feather          · ti-book

   ACCESSIBILITY. Every layer this pass mounts is decorative, so the mount
   contract now requires `aria-hidden="true"` on the layer element — a screen
   reader announcing fourteen icon glyphs is worse than no decoration at all.
   =========================================================================== */

/* ---------------------------------------------------------------------------
   5.1 · GLYPH PARTICLES
   Markup: <i class="ti ti-leaf fx-p fx-g" style="--x:…;--y:…;--d:…;--sc:…"></i>
   inside the engine's existing .fx-layer. Mix freely with geometric .fx-p — the
   Forgewright looks best with both, sparks among the cinders.
   --------------------------------------------------------------------------- */
.fx-p.fx-g {
  /* cancel the geometric paint, keep the engine's animation */
  background: none !important;
  border: 0 !important;
  box-shadow: none !important;
  border-radius: 0 !important;
  width: auto !important;
  height: auto !important;
  font-size: var(--gsz, 13px);
  line-height: 1;
  /* .fx-p already sets color:var(--fx-tint) — the glyph tints with the engine */
  opacity: var(--go, 0.85);
}
/* A glyph reads at a different weight than a 2px dot, so the engines that were
   tuned for dots get their glyph variant thinned rather than every profession
   being re-tuned. */
.fx-bone-ashfall > .fx-g { --gsz: 10px; --go: 0.5; }
.fx-scribe-motes > .fx-g { --gsz: 11px; --go: 0.55; }
.fx-pros-lantern > .fx-g { --gsz: 12px; --go: 0.7; }
.fx-api-swarm > .fx-g    { --gsz: 9px; }
.fx-glass-glint > .fx-g  { --gsz: 16px; --go: 1; }
.fx-forge-cinder > .fx-g { --gsz: 12px; }
.fx-cook-steam > .fx-g   { --gsz: 15px; --go: 0.6; }
.fx-alch-vapour > .fx-g  { --gsz: 18px; --go: 0.5; }

/* Forgewright is the one engine whose subject is a PAINT property — its cinders
   cool from white through orange to dead red. A glyph has no background to ramp,
   so the ramp moves to `color` for glyph cinders only. Same curve, same
   duration, so a glyph and a dot cool in step. */
.fx-forge-cinder > .fx-g {
  animation: forge-cinder 8.5s cubic-bezier(.12, .62, .3, 1) infinite,
             forge-cinder-glyph 8.5s cubic-bezier(.12, .62, .3, 1) infinite;
}
@keyframes forge-cinder-glyph {
  0%   { color: #fff6e0; }
  46%  { color: #ff8a3c; }
  100% { color: #a32718; }
}
/* Ditto Tidecaller, whose engine has no particles at all: glyphs get their own
   slow rise so the caustic net can carry shells and droplets without the net
   itself gaining a particle layer. */
.fx-tide-caustic > .fx-g {
  animation: tide-caustic-glyph 21s cubic-bezier(.36, .06, .5, .95) infinite;
}
@keyframes tide-caustic-glyph {
  0%   { transform: translate3d(0, 12vh, 0) rotate(-12deg) scale(var(--sc, 1)); opacity: 0; }
  18%  { opacity: 0.7; }
  82%  { opacity: 0.6; }
  100% { transform: translate3d(6vw, -34vh, 0) rotate(14deg) scale(var(--sc, 1)); opacity: 0; }
}

/* ---------------------------------------------------------------------------
   5.2 · CORNER FLOURISHES
   One engine, two knobs. Markup, mounted per card / post / panel:

     <span class="fx-corners" aria-hidden="true">
       <i class="ti ti-leaf fx-cn k1"></i> … k2 k3 k4
     </span>

   The host needs position:relative, which every surface this lands on already
   has. Corners are STATIC by default and only move on the profession's chosen
   motion — a mark in all four corners of twelve cards is the single easiest way
   to make a page look busy, so the default is stillness.
   --------------------------------------------------------------------------- */
.fx-corners { position: absolute; inset: 0; pointer-events: none; z-index: 2; border-radius: inherit; }
.fx-cn {
  position: absolute;
  font-size: var(--cn-sz, 11px);
  line-height: 1;
  color: var(--cn-tint, var(--border-accent));
  opacity: var(--cn-o, 0.5);
}
.fx-cn.k1 { left: var(--cn-in, 4px); top: var(--cn-in, 4px); }
.fx-cn.k2 { right: var(--cn-in, 4px); top: var(--cn-in, 4px); transform: scaleX(-1); }
.fx-cn.k3 { right: var(--cn-in, 4px); bottom: var(--cn-in, 4px); transform: scale(-1); }
.fx-cn.k4 { left: var(--cn-in, 4px); bottom: var(--cn-in, 4px); transform: scaleY(-1); }

/* Only two corners on a small surface. Four marks on a chip is a border. */
.fx-corners.pair .k2, .fx-corners.pair .k4 { display: none; }

/* The four motions, assigned per profession below. Each is on the GLYPH, never
   on the host, so nothing here can shift a layout. */
@keyframes cn-turn   { to { transform: rotate(360deg); } }
@keyframes cn-breathe{ 0%, 100% { opacity: var(--cn-o, .5); } 50% { opacity: 1; } }
@keyframes cn-nod    { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2px); } }
@keyframes cn-flicker{ 0%, 100% { opacity: var(--cn-o, .5); } 42% { opacity: .95; } 55% { opacity: .3; } 70% { opacity: .85; } }

/* still — Prospector, Woodwright, Homesteader, Weaver, Scribe:
   the crafts whose mark is a maker's mark, not a light. Nothing to declare. */
/* breathe — Herbalist, Mycologist, Apiarist */
html[data-on-herb-press] .fx-cn,
html[data-on-myco-rot] .fx-cn,
html[data-on-api-cell] .fx-cn { animation: cn-breathe 5.5s ease-in-out infinite; }
html[data-on-myco-rot] .fx-cn.k2 { animation-delay: -1.8s; }
html[data-on-myco-rot] .fx-cn.k3 { animation-delay: -3.6s; }
/* nod — Tidecaller, Beastmaster */
html[data-on-tide-ripple] .fx-cn,
html[data-on-beast-stalked] .fx-cn { animation: cn-nod 4.2s ease-in-out infinite; }
/* flicker — Forgewright, Cook, Glassworker */
html[data-on-forge-billet] .fx-cn,
html[data-on-cook-menu] .fx-cn,
html[data-on-glass-cabochon] .fx-cn { animation: cn-flicker 3.4s ease-in-out infinite; }
/* turn — Alchemist, Bone-hunter. Very slow; a turning mark is a mechanism. */
html[data-on-alch-decant] .fx-cn,
html[data-on-bone-scavenged] .fx-cn { animation: cn-turn 34s linear infinite; }

/* ---------------------------------------------------------------------------
   5.3 · BORDER CRAWL
   One glyph walking the perimeter. Markup:

     <span class="fx-crawl" aria-hidden="true"><i class="ti ti-leaf"></i></span>

   DELIBERATELY NOT ON CARDS. A page can hold twelve cards and twelve crawlers
   animating top/left is twelve elements off the compositor; it goes on the page
   TITLE and the POST FRAME, which are one apiece. The waypoints are percentages
   of the host, so it fits any box without measurement.
   --------------------------------------------------------------------------- */
.fx-crawl { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.fx-crawl > i {
  position: absolute;
  font-size: var(--cr-sz, 12px);
  line-height: 1;
  color: var(--cr-tint, var(--border-accent));
  opacity: var(--cr-o, 0.7);
  animation: crawl-perimeter var(--cr-dur, 14s) linear infinite;
}
@keyframes crawl-perimeter {
  0%   { left: 0;    top: 0;    transform: rotate(0deg); }
  25%  { left: 100%; top: 0;    transform: rotate(90deg); }
  50%  { left: 100%; top: 100%; transform: rotate(180deg); }
  75%  { left: 0;    top: 100%; transform: rotate(270deg); }
  100% { left: 0;    top: 0;    transform: rotate(360deg); }
}
/* Two crawlers on opposite corners read as a pair working round rather than as
   one lost glyph. Opt in with .fx-crawl.two and a second <i>. */
.fx-crawl.two > i:nth-child(2) { animation-delay: calc(var(--cr-dur, 14s) / -2); }

/* Per-profession pace. The only knob, and it does most of the character: a
   Beastmaster's mark prowls, a Scribe's is barely moving. */
html[data-on-pros-seam] .fx-crawl > i     { --cr-dur: 17s; }
html[data-on-herb-creeper] .fx-crawl > i  { --cr-dur: 22s; }
html[data-on-wood-planed] .fx-crawl > i   { --cr-dur: 19s; }
html[data-on-tide-waterline] .fx-crawl > i{ --cr-dur: 13s; }
html[data-on-bone-etched] .fx-crawl > i   { --cr-dur: 26s; --cr-o: .5; }
html[data-on-beast-scent] .fx-crawl > i   { --cr-dur: 9s; }
html[data-on-home-fenceline] .fx-crawl > i{ --cr-dur: 28s; }
html[data-on-api-filling] .fx-crawl > i   { --cr-dur: 6s; }
html[data-on-myco-threads] .fx-crawl > i  { --cr-dur: 31s; }
html[data-on-alch-titrate] .fx-crawl > i  { --cr-dur: 15s; }
html[data-on-glass-frosted] .fx-crawl > i { --cr-dur: 11s; }
html[data-on-forge-tang] .fx-crawl > i    { --cr-dur: 8s; }
html[data-on-weave-selvedge] .fx-crawl > i{ --cr-dur: 20s; }
html[data-on-cook-mise] .fx-crawl > i     { --cr-dur: 12s; }
html[data-on-scribe-margin] .fx-crawl > i { --cr-dur: 44s; --cr-o: .45; }

/* ---------------------------------------------------------------------------
   5.4 · CHIPS, REWORKED
   The chip treatments in pass 3 were clip-paths and gradients — abstract, and
   the weakest thing in the set. A chip is a LABEL, and a label wants a mark.
   Markup: the chip gains a leading <i class="ti ti-x chip-glyph"></i>.
   --------------------------------------------------------------------------- */
.chip-glyph {
  font-size: 11px;
  line-height: 1;
  margin-right: 1px;
  opacity: 0.8;
  color: var(--cls-color, var(--text-accent));
  flex: none;
}
/* The pass-3 chip silhouettes stay — they are the chip's SHAPE — and the glyph
   sits inside them. Only the two that drew their own mark give it up, since a
   glyph and a drawn stem in the same 15px is a collision. */
html[data-on-herb-graft] .chip::before,
html[data-on-herb-graft] .loot-chip::before { content: none; }
html[data-on-herb-graft] .chip,
html[data-on-herb-graft] .loot-chip { padding-left: 8px; }

/* Rarity marks get the glyph too, which is where it earns most: a Legendary row
   currently says "Legendary" in gold and nothing else. */
.loot-rarity .chip-glyph { opacity: 1; }

/* ---------------------------------------------------------------------------
   5.5 · BADGES, REWORKED
   Pass 3's badge work was rings and masks. A badge is the one surface where a
   SYMBOL belongs most, so: an orbiting glyph, and a glyph seated at the foot of
   the disc like a maker's stamp. Both on the WRAPPER, never on .bdg — no
   transform, filter, opacity or z-index on the badge root, or the rank ring dies.
   Markup: <span class="bdg-orbit" aria-hidden="true"><i class="ti ti-x"></i></span>
   as a sibling of .bdg inside its wrapper.
   --------------------------------------------------------------------------- */
.bdg-orbit {
  position: absolute;
  inset: -18%;
  pointer-events: none;
  animation: bdg-orbit-turn var(--orb-dur, 11s) linear infinite;
}
.bdg-orbit > i {
  position: absolute;
  top: 0;
  left: 50%;
  font-size: var(--orb-sz, 10px);
  line-height: 1;
  margin-left: calc(var(--orb-sz, 10px) / -2);
  color: var(--cls-color, var(--text-accent));
  /* counter-rotate so the glyph stays upright while the arm turns */
  animation: bdg-orbit-turn var(--orb-dur, 11s) linear infinite reverse;
}
@keyframes bdg-orbit-turn { to { transform: rotate(360deg); } }

/* Seated glyph — a stamp at the foot of the disc. Static, and it is the half of
   this that survives `calm` and reduced motion. */
.bdg-seal {
  position: absolute;
  left: 50%;
  bottom: -8%;
  transform: translateX(-50%);
  font-size: var(--seal-sz, 10px);
  line-height: 1;
  color: var(--cls-color, var(--text-accent));
  opacity: 0.85;
  pointer-events: none;
  text-shadow: 0 0 4px var(--surface-0);
}

/* Pace per profession, so an orbiting mark reads as the craft rather than as a
   spinner. Apiarist's is fast enough to blur; Scribe's takes most of a minute. */
html[data-on-api-veil] .bdg-orbit      { --orb-dur: 3.4s; }
html[data-on-forge-struck] .bdg-orbit  { --orb-dur: 6s; }
html[data-on-beast-hackles] .bdg-orbit { --orb-dur: 8s; }
html[data-on-glass-refract] .bdg-orbit { --orb-dur: 9s; }
html[data-on-tide-submerged] .bdg-orbit{ --orb-dur: 14s; }
html[data-on-myco-halo] .bdg-orbit     { --orb-dur: 19s; }
html[data-on-scribe-seal] .bdg-orbit   { --orb-dur: 52s; }

/* ---------------------------------------------------------------------------
   5.6 · GUARDS
   Same two as everywhere else. Note what STAYS: every glyph, every corner mark,
   every seal. Only the motion goes — which is the whole reason the corners
   default to still and the seal is static.
   --------------------------------------------------------------------------- */
html[data-intensity="calm"] .fx-cn,
html[data-intensity="calm"] .fx-crawl > i,
html[data-intensity="calm"] .bdg-orbit,
html[data-intensity="calm"] .bdg-orbit > i { animation: none !important; }

@media (prefers-reduced-motion: reduce) {
  .fx-cn, .fx-crawl > i, .bdg-orbit, .bdg-orbit > i,
  .fx-p.fx-g, .fx-forge-cinder > .fx-g, .fx-tide-caustic > .fx-g { animation: none !important; }
  /* A crawler frozen mid-perimeter looks like a mistake; park it. */
  .fx-crawl > i { left: 0; top: 0; }
}


/* ===========================================================================
   PASS 6: FRAMES — a border ornament per profession, and two new animations.

   Game UI frames the important thing. Passes 1-4 gave every profession an edge
   treatment, but they were mostly a colour and a glow; this gives each one an
   ORNAMENT with a repeat, which is what makes a panel read as forged, woven,
   ruled or grown rather than as a div with a border.

   ---------------------------------------------------------------------------
   THREE TECHNIQUE DECISIONS, and why each is the one that works here.

   1. LAYERED BACKGROUNDS, NOT border-image. `border-image` is the obvious tool
      and it is the wrong one: border-radius has no effect on a border image, and
      every card, post and panel in this app is rounded by `--radius`. So the
      ornament is painted as a background on the BORDER box with the surface
      painted over the padding box:

        border: var(--pf-w) solid transparent;
        background-image: linear-gradient(surface, surface), <ornament layers>;
        background-origin: padding-box, border-box;
        background-clip:  padding-box, border-box;

      Radius is respected, no extra element, no pseudo spent.

   2. FOUR EDGE LAYERS, NOT ONE. A repeating gradient runs along one axis, so an
      ornament that goes round needs one layer per edge, sized and positioned to
      it. This is exactly the shape of `fx-march` in effects.css — the same four
      background-size / -position / -repeat triples — so it is a pattern already
      in the codebase rather than a new idea.

   3. outline + outline-offset FOR THE SECOND RULE. A double frame is the single
      most common game-UI border, and `outline` draws one outside the box for
      free: it takes no part in the box model, so it cannot shift a layout, and
      it needs no pseudo-element. `--pf-gap` is the breath between the rules.

   ---------------------------------------------------------------------------
   THE TWO NEW ANIMATIONS. Both are drawn from game UI rather than web UI, and
   both are STATE animations — they say "this one, now", which is what a frame is
   for. Neither is an idle loop by default.

     · TRACE   the frame draws itself round the box, once. A conic mask sweeps
               from the top-left and the ornament appears behind it. This is the
               selection/focus convention in almost every menu-driven game.
     · CHARGE  the ornament's ticks light in SEQUENCE, like a bar filling or a
               rune warming up. steps(), because a segmented readout counts.

   CHARGE MOUNTS A CHILD SPAN, NOT ::after, and that is not a style preference.
   Every profession's pass-3 card placement already owns .record-card::after —
   the first version of this pass claimed it too, won on specificity, and
   silently deleted the shipped effect on any surface that opted in (Herbalist's
   sprout, measured, simply stopped existing). A pseudo-element is a single slot
   and passes 1-4 spent it. So the host mounts:

     <span class="pf-lit" aria-hidden="true"></span>

   as a child, the same way .fx-corners and .fx-crawl are mounted, and the two
   layers composite instead of competing.

   Each profession gets one, chosen by which suits the craft: things that are
   MADE trace (the frame is built), things that are LIT charge.
   =========================================================================== */

@property --pf-p { syntax: '<percentage>'; initial-value: 0%; inherits: false; }

/* ---------------------------------------------------------------------------
   6.1 · THE FRAME
   Add .pf to any card, post or panel. The profession's ornament comes from
   --pf-h / --pf-v, set per profession below.
   --------------------------------------------------------------------------- */
.pf {
  position: relative;
  border: var(--pf-w, 3px) solid transparent;
  background-image:
    linear-gradient(var(--pf-fill, var(--surface-1)), var(--pf-fill, var(--surface-1))),
    var(--pf-h, none), var(--pf-h, none), var(--pf-v, none), var(--pf-v, none);
  background-size:
    auto auto,
    auto var(--pf-w, 3px), auto var(--pf-w, 3px),
    var(--pf-w, 3px) auto, var(--pf-w, 3px) auto;
  background-position: 0 0, 0 0, 0 100%, 0 0, 100% 0;
  background-repeat: no-repeat, repeat-x, repeat-x, repeat-y, repeat-y;
  background-origin: padding-box, border-box, border-box, border-box, border-box;
  background-clip: padding-box, border-box, border-box, border-box, border-box;
}
/* The second rule. Outside the box, costs no layout. */
.pf.pf-double {
  outline: 1px solid var(--pf-rule, var(--border-accent));
  outline-offset: var(--pf-gap, 3px);
}

/* ---------------------------------------------------------------------------
   6.2 · TRACE — the frame draws itself round, once.
   A conic mask sweeps clockwise from the top-left; the ornament is revealed
   behind it. One element, one animated custom property, no extra markup.
   Add .pf-trace. It fires on mount, and again whenever the host gains .is-new
   or .selected — the app decides which.
   --------------------------------------------------------------------------- */
.pf-trace {
  -webkit-mask: conic-gradient(from -45deg, #000 0 var(--pf-p, 0%), transparent var(--pf-p, 0%));
          mask: conic-gradient(from -45deg, #000 0 var(--pf-p, 0%), transparent var(--pf-p, 0%));
  animation: pf-trace var(--pf-dur, 1.1s) cubic-bezier(.4, .05, .3, 1) forwards;
}
@keyframes pf-trace { from { --pf-p: 0%; } to { --pf-p: 100%; } }
/* Where @property is unsupported the mask never resolves a mid value, so the
   frame simply appears whole — deliberate, and it still looks intentional. */
@supports not (background: paint(x)) {
  .pf-trace { -webkit-mask: none; mask: none; }
}

/* ---------------------------------------------------------------------------
   6.3 · CHARGE — the ornament's ticks light in sequence.
   A brightening overlay walks the top edge in discrete steps. steps(), because
   a segmented readout counts rather than glides.
   Add .pf-charge. Idles slowly; the app can speed it with --pf-cdur on a state.
   --------------------------------------------------------------------------- */
.pf-charge > .pf-lit {
  position: absolute;
  inset: calc(var(--pf-w, 3px) * -1);
  border-radius: inherit;
  pointer-events: none;
  background-image: linear-gradient(90deg, var(--pf-lit, rgba(255,255,255,.35)) 0 12%, transparent 12%);
  background-size: 100% var(--pf-w, 3px);
  background-repeat: no-repeat;
  animation: pf-charge var(--pf-cdur, 3.6s) steps(8, end) infinite;
}
@keyframes pf-charge {
  from { background-position: -12% 0; }
  to   { background-position: 112% 0; }
}

/* ---------------------------------------------------------------------------
   6.4 · THE ORNAMENTS — one per profession.
   Each is a pair of repeating gradients: --pf-h runs the top and bottom edges,
   --pf-v the left and right. Keeping them separate is what lets a fence have
   posts on the sides and rails across the top rather than the same tick
   everywhere.
   Bound to the profession's EXISTING card and post-frame effect ids, so this
   upgrades what is already equipped rather than adding fifteen shop rows.
   --------------------------------------------------------------------------- */

/* Prospector — faceted ticks, cut at an angle and unevenly spaced. */
html[data-on-pros-corelog] .pf, html[data-on-frame-assayed] .pf {
  --pf-w: 3px; --pf-rule: rgba(232,161,60,.45);
  --pf-h: repeating-linear-gradient(72deg, rgba(255,232,180,.8) 0 2px, transparent 2px 5px, rgba(232,161,60,.5) 5px 6px, transparent 6px 13px);
  --pf-v: repeating-linear-gradient(18deg, rgba(255,232,180,.7) 0 2px, transparent 2px 5px, rgba(232,161,60,.45) 5px 6px, transparent 6px 13px);
  --pf-lit: rgba(255,240,200,.5);
}
/* Herbalist — a vine: long leaf, gap, short leaf. Never even. */
html[data-on-herb-press] .pf, html[data-on-back-hedgerow] .pf {
  --pf-w: 3px; --pf-rule: rgba(116,207,138,.4);
  --pf-h: repeating-linear-gradient(90deg, rgba(116,207,138,.85) 0 6px, transparent 6px 9px, rgba(158,224,138,.6) 9px 12px, transparent 12px 21px);
  --pf-v: repeating-linear-gradient(0deg, rgba(116,207,138,.85) 0 6px, transparent 6px 9px, rgba(158,224,138,.6) 9px 12px, transparent 12px 21px);
  --pf-lit: rgba(190,240,190,.45);
}
/* Woodwright — dovetail teeth. Square, and the sides interlock the top. */
html[data-on-wood-joint] .pf, html[data-on-frame-mitred] .pf {
  --pf-w: 4px; --pf-rule: rgba(201,138,69,.5);
  --pf-h: repeating-linear-gradient(90deg, rgba(201,138,69,.9) 0 7px, transparent 7px 11px);
  --pf-v: repeating-linear-gradient(0deg, transparent 0 4px, rgba(201,138,69,.9) 4px 11px, transparent 11px 15px);
  --pf-lit: rgba(236,214,180,.4);
}
/* Tidecaller — a swell: alternating long and short, so the edge rises and falls. */
html[data-on-tide-meniscus] .pf, html[data-on-back-shallows] .pf {
  --pf-w: 3px; --pf-rule: rgba(63,169,216,.45);
  --pf-h: repeating-linear-gradient(90deg, rgba(127,227,224,.85) 0 9px, rgba(63,169,216,.5) 9px 13px, transparent 13px 15px, rgba(63,169,216,.5) 15px 18px);
  --pf-v: repeating-linear-gradient(0deg, rgba(127,227,224,.85) 0 9px, rgba(63,169,216,.5) 9px 13px, transparent 13px 15px, rgba(63,169,216,.5) 15px 18px);
  --pf-lit: rgba(215,248,252,.5);
}
/* Bone-hunter — rib ticks. Short, cold, and widely spaced. */
html[data-on-bone-scavenged] .pf, html[data-on-back-interred] .pf {
  --pf-w: 3px; --pf-rule: rgba(206,206,216,.35);
  --pf-h: repeating-linear-gradient(90deg, rgba(226,226,234,.8) 0 1.5px, transparent 1.5px 9px);
  --pf-v: repeating-linear-gradient(0deg, rgba(206,206,216,.65) 0 1.5px, transparent 1.5px 9px);
  --pf-lit: rgba(240,240,246,.35);
}
/* Beastmaster — claw: groups of three close marks, then a long gap. */
html[data-on-beast-stalked] .pf, html[data-on-badge-collared] .pf {
  --pf-w: 3px; --pf-rule: rgba(176,122,240,.45);
  --pf-h: repeating-linear-gradient(76deg, rgba(176,122,240,.9) 0 2px, transparent 2px 5px, rgba(176,122,240,.9) 5px 7px, transparent 7px 10px, rgba(255,164,100,.7) 10px 12px, transparent 12px 26px);
  --pf-v: repeating-linear-gradient(14deg, rgba(176,122,240,.9) 0 2px, transparent 2px 5px, rgba(176,122,240,.9) 5px 7px, transparent 7px 10px, rgba(255,164,100,.7) 10px 12px, transparent 12px 26px);
  --pf-lit: rgba(211,170,255,.5);
}
/* Homesteader — a fence: rails across the top, posts up the sides. */
html[data-on-home-shelf] .pf, html[data-on-frame-posted] .pf {
  --pf-w: 3px; --pf-rule: rgba(106,167,156,.4);
  --pf-h: repeating-linear-gradient(0deg, rgba(106,167,156,.75) 0 1px, transparent 1px 2px, rgba(106,167,156,.75) 2px 3px);
  --pf-v: repeating-linear-gradient(0deg, rgba(106,167,156,.8) 0 3px, transparent 3px 22px);
  --pf-lit: rgba(226,214,168,.35);
}
/* Apiarist — comb, at sixty degrees so the cells read on the edge. */
html[data-on-api-cell] .pf, html[data-on-frame-sealed] .pf {
  --pf-w: 4px; --pf-rule: rgba(242,115,155,.45);
  --pf-h: repeating-linear-gradient(60deg, rgba(255,194,122,.85) 0 1.5px, transparent 1.5px 7px);
  --pf-v: repeating-linear-gradient(-60deg, rgba(255,194,122,.85) 0 1.5px, transparent 1.5px 7px);
  --pf-lit: rgba(255,222,190,.55);
}
/* Mycologist — gills. The densest ornament here, and the only one with no gap
   wide enough to read as a dash. */
html[data-on-myco-rot] .pf, html[data-on-back-spotted] .pf {
  --pf-w: 3px; --pf-rule: rgba(255,111,97,.4);
  --pf-h: repeating-linear-gradient(90deg, rgba(255,111,97,.8) 0 1px, transparent 1px 3px);
  --pf-v: repeating-linear-gradient(0deg, rgba(192,132,232,.7) 0 1px, transparent 1px 3px);
  --pf-lit: rgba(255,160,145,.5);
}
/* Alchemist — a graduated scale: a long mark every fifth short one. */
html[data-on-alch-decant] .pf, html[data-on-back-suspension] .pf {
  --pf-w: 4px; --pf-rule: rgba(123,139,245,.45);
  --pf-h: repeating-linear-gradient(90deg, rgba(212,216,244,.75) 0 1px, transparent 1px 6px),
          repeating-linear-gradient(90deg, rgba(103,224,168,.8) 0 1.5px, transparent 1.5px 30px);
  --pf-v: repeating-linear-gradient(0deg, rgba(212,216,244,.75) 0 1px, transparent 1px 6px);
  --pf-lit: rgba(230,255,245,.5);
}
/* Glassworker — leading: a fine double line, the two rules of a came. */
html[data-on-glass-cabochon] .pf, html[data-on-name-leaded] .pf {
  --pf-w: 4px; --pf-rule: rgba(99,220,237,.5); --pf-gap: 2px;
  --pf-h: linear-gradient(0deg, rgba(214,244,252,.7) 0 1px, transparent 1px 3px, rgba(99,220,237,.6) 3px 4px);
  --pf-v: linear-gradient(90deg, rgba(214,244,252,.7) 0 1px, transparent 1px 3px, rgba(99,220,237,.6) 3px 4px);
  --pf-lit: rgba(255,255,255,.6);
}
/* Forgewright — rivets: a dot at intervals over a dark seam. */
html[data-on-forge-billet] .pf, html[data-on-frame-welded] .pf {
  --pf-w: 5px; --pf-rule: rgba(255,106,43,.5);
  --pf-h: radial-gradient(circle 2px at 9px 50%, rgba(255,240,210,.95) 0 60%, transparent 62%),
          linear-gradient(0deg, rgba(90,30,16,.9), rgba(160,60,26,.9));
  --pf-v: radial-gradient(circle 2px at 50% 9px, rgba(255,240,210,.95) 0 60%, transparent 62%),
          linear-gradient(90deg, rgba(90,30,16,.9), rgba(160,60,26,.9));
  --pf-lit: rgba(255,246,224,.6);
}
/* Weaver — a braid: two runs of 45deg ticks leaning opposite ways. */
html[data-on-weave-basted] .pf, html[data-on-frame-bound] .pf {
  --pf-w: 4px; --pf-rule: rgba(226,99,189,.45);
  --pf-h: repeating-linear-gradient(45deg, rgba(226,99,189,.85) 0 2px, transparent 2px 5px),
          repeating-linear-gradient(-45deg, rgba(245,143,212,.6) 0 2px, transparent 2px 5px);
  --pf-v: repeating-linear-gradient(45deg, rgba(226,99,189,.85) 0 2px, transparent 2px 5px),
          repeating-linear-gradient(-45deg, rgba(245,143,212,.6) 0 2px, transparent 2px 5px);
  --pf-lit: rgba(255,200,240,.5);
}
/* Cook — a rope: alternating fat and thin, which is the twist. */
html[data-on-cook-menu] .pf, html[data-on-back-scorched] .pf {
  --pf-w: 4px; --pf-rule: rgba(158,207,66,.4);
  --pf-h: repeating-linear-gradient(66deg, rgba(240,212,136,.9) 0 3px, rgba(158,207,66,.5) 3px 5px, transparent 5px 8px);
  --pf-v: repeating-linear-gradient(24deg, rgba(240,212,136,.9) 0 3px, rgba(158,207,66,.5) 3px 5px, transparent 5px 8px);
  --pf-lit: rgba(243,247,230,.5);
}
/* Scribe — ruled: one hairline, and a dot in the margin at intervals. The
   quietest frame in the set, which is the element. */
html[data-on-scribe-margin] .pf, html[data-on-name-engrossed] .pf {
  --pf-w: 3px; --pf-rule: rgba(139,164,201,.3); --pf-gap: 4px;
  --pf-h: radial-gradient(circle 1px at 16px 50%, rgba(221,196,140,.85) 0 60%, transparent 62%),
          linear-gradient(0deg, transparent 0 1px, rgba(139,164,201,.55) 1px 2px, transparent 2px);
  --pf-v: radial-gradient(circle 1px at 50% 16px, rgba(221,196,140,.85) 0 60%, transparent 62%),
          linear-gradient(90deg, transparent 0 1px, rgba(139,164,201,.55) 1px 2px, transparent 2px);
  --pf-lit: rgba(240,232,210,.4);
}

/* ---------------------------------------------------------------------------
   6.5 · WHICH ANIMATION, AND WHY
   Opt-in per surface via .pf-anim. The ornament in 6.4 is unconditional paint,
   but a conic mask applies to an element's CHILDREN too, so a traced card wipes
   its own content in — lovely on one post, a flickering page on twelve cards.
   The surface asks for the animation; the profession decides which it gets.
   Things that are MADE trace — the frame assembles, because that is what the
   craft does. Things that are LIT charge — the ornament warms in sequence.
   The split is the craft's own verb, not a coin toss.
   --------------------------------------------------------------------------- */
/* trace — built: dovetails, rivets, braid, comb, leading, a drawn rule, a cut facet */
html[data-on-wood-joint] .pf.pf-anim,      html[data-on-frame-mitred] .pf.pf-anim,
html[data-on-forge-billet] .pf.pf-anim,    html[data-on-frame-welded] .pf.pf-anim,
html[data-on-weave-basted] .pf.pf-anim,    html[data-on-frame-bound] .pf.pf-anim,
html[data-on-api-cell] .pf.pf-anim,        html[data-on-frame-sealed] .pf.pf-anim,
html[data-on-glass-cabochon] .pf.pf-anim,  html[data-on-name-leaded] .pf.pf-anim,
html[data-on-scribe-margin] .pf.pf-anim,   html[data-on-name-engrossed] .pf.pf-anim,
html[data-on-pros-corelog] .pf.pf-anim,    html[data-on-frame-assayed] .pf.pf-anim {
  /* :where(.pf-anim) keeps this at .pf specificity while requiring the opt-in */
  -webkit-mask: conic-gradient(from -45deg, #000 0 var(--pf-p, 100%), transparent var(--pf-p, 100%));
          mask: conic-gradient(from -45deg, #000 0 var(--pf-p, 100%), transparent var(--pf-p, 100%));
  animation: pf-trace var(--pf-dur, 1.1s) cubic-bezier(.4, .05, .3, 1) forwards;
}
/* charge — lit: growth, tide, rot, reaction, hearth-fire, a stalking shadow, ash */
html[data-on-herb-press] .pf.pf-anim > .pf-lit,   html[data-on-back-hedgerow] .pf.pf-anim > .pf-lit,
html[data-on-tide-meniscus] .pf.pf-anim > .pf-lit,html[data-on-back-shallows] .pf.pf-anim > .pf-lit,
html[data-on-myco-rot] .pf.pf-anim > .pf-lit,     html[data-on-back-spotted] .pf.pf-anim > .pf-lit,
html[data-on-alch-decant] .pf.pf-anim > .pf-lit,  html[data-on-back-suspension] .pf.pf-anim > .pf-lit,
html[data-on-cook-menu] .pf.pf-anim > .pf-lit,    html[data-on-back-scorched] .pf.pf-anim > .pf-lit,
html[data-on-beast-stalked] .pf.pf-anim > .pf-lit,html[data-on-badge-collared] .pf.pf-anim > .pf-lit,
html[data-on-bone-scavenged] .pf.pf-anim > .pf-lit,html[data-on-back-interred] .pf.pf-anim > .pf-lit,
html[data-on-home-shelf] .pf.pf-anim > .pf-lit,   html[data-on-frame-posted] .pf.pf-anim > .pf-lit {
  position: absolute;
  inset: calc(var(--pf-w, 3px) * -1);
  border-radius: inherit;
  pointer-events: none;
  background-image: linear-gradient(90deg, var(--pf-lit, rgba(255,255,255,.35)) 0 12%, transparent 12%);
  background-size: 100% var(--pf-w, 3px);
  background-repeat: no-repeat;
  animation: pf-charge var(--pf-cdur, 3.6s) steps(8, end) infinite;
}
/* Pace by craft. A hive charges fast, ash barely at all. */
html[data-on-tide-meniscus] .pf.pf-anim > .pf-lit  { --pf-cdur: 5s; }
html[data-on-bone-scavenged] .pf.pf-anim > .pf-lit { --pf-cdur: 9s; }
html[data-on-home-shelf] .pf.pf-anim > .pf-lit     { --pf-cdur: 7s; }
html[data-on-myco-rot] .pf.pf-anim > .pf-lit       { --pf-cdur: 6.5s; }
html[data-on-beast-stalked] .pf.pf-anim > .pf-lit  { --pf-cdur: 2.6s; }

/* ---------------------------------------------------------------------------
   6.6 · GUARDS
   The ornament is paint and stays. Trace resolves to a whole frame rather than a
   partial one, which is the only way a one-shot reveal can degrade honestly.
   --------------------------------------------------------------------------- */
html[data-intensity="calm"] .pf > .pf-lit { animation: none !important; }
html[data-intensity="calm"] .pf { -webkit-mask: none !important; mask: none !important; animation: none !important; }

@media (prefers-reduced-motion: reduce) {
  .pf, .pf-trace { -webkit-mask: none !important; mask: none !important; animation: none !important; }
  .pf > .pf-lit, .pf-charge > .pf-lit { animation: none !important; }
}
