/* ============================================================
   ILLUSTRATION KIT (K3, DMITRY-REDESIGN-2026-09)
   Mechanics for every bespoke animated illustration on the site: draw-in on
   arrival, one gentle living loop, one meaningful hover. Full contract and
   worked examples: DMITRY-REDESIGN-2026-09/ILLUSTRATION-GUIDE.md. Runtime:
   assets/js/illus-kit.js (defer, already linked on every page).

   Token contract (shared with every other kit builder; K1 defines the real
   values in fable.css :root, this file only ever reads them through a
   literal fallback so it renders correctly even before K1 lands):
     --navy-ground   #0C1833   the one dark ground
     --crimson-vivid #E23A46   accent lines/fills on navy
     --crimson-glow  #FF6B75   small crimson text on navy
     --ink-on-dark   #F4F7FB   headings/ice lines on navy
   Everything else (line colour on paper, glass fill tint) reads the site's
   existing --ink / --paper-2 tokens directly, since those already exist.

   Zero em or en dashes anywhere, including these comments.
   ============================================================ */

/* ---------- 1. root + colour roles (automatic by ground) ---------- */
.il{
  display:block;width:100%;height:auto;overflow:visible;
  --il-line:var(--ink,#0B1B33);
  --il-fill:rgb(11 27 51 / .05);
  --il-tile:#FFFFFF;
  --il-accent:var(--crimson-vivid,#E23A46);
  --il-accent-glow:rgba(226,58,70,.5);
}
/* Dark grounds: ice white line, faint glass fill, crimson accent keeps its
   own vivid value (it already reads on navy). Matches the same ground list
   fable.css already keys --hair off, so a page builder never has to add a
   second class: whatever already makes a section "dark" makes its .il dark
   too. */
:is(.sec-dark,.on-dark,.phero--dark,.glass-1,.glass-2,.glass-3) .il{
  --il-line:var(--ink-on-dark,#F4F7FB);
  --il-fill:rgb(255 255 255 / .08);
  --il-tile:#1C2844;
  --il-accent:var(--crimson-vivid,#E23A46);
  --il-accent-glow:rgba(226,58,70,.55);
}
/* Crimson ground is a special case: the vivid crimson accent would vanish
   against a crimson field, so line, fill AND accent all move to ice white,
   the way the site's existing .ill system already solves the same problem
   (fable.css, .sec-crimson .ill .dr-c). */
.sec-crimson .il{
  --il-line:var(--paper-2,#FFFFFF);
  --il-fill:rgb(255 255 255 / .14);
  --il-tile:#C9404A;
  --il-accent:var(--paper-2,#FFFFFF);
  --il-accent-glow:rgba(255,255,255,.6);
}
/* Explicit escape hatch: automatic ground detection reads the section's own
   .sec-dark / .on-dark / .phero--dark class, which is correct for every real
   page (that class is already there for everything else on the ground). The
   rare spot where an illustration sits on a dark tile with none of those
   ancestor classes (a glass panel dropped into an otherwise light section,
   for one) can force it directly: add .il--dark or .il--light to the <svg
   class="il"> itself, or to any ancestor of it. */
.il.il--dark,.il--dark .il{
  --il-line:var(--ink-on-dark,#F4F7FB);
  --il-fill:rgb(255 255 255 / .08);
  --il-tile:#1C2844;
  --il-accent:var(--crimson-vivid,#E23A46);
  --il-accent-glow:rgba(226,58,70,.55);
}
.il.il--light,.il--light .il{
  --il-line:var(--ink,#0B1B33);
  --il-fill:rgb(11 27 51 / .05);
  --il-tile:#FFFFFF;
  --il-accent:var(--crimson-vivid,#E23A46);
  --il-accent-glow:rgba(226,58,70,.5);
}

/* ---------- 2. line, fill and accent primitives ---------- */
.il :is(path,line,polyline,polygon,circle,ellipse,rect){
  fill:none;stroke:var(--il-line);stroke-width:var(--il-sw,1.8);
  stroke-linecap:round;stroke-linejoin:round;
}
/* the ONE accent per scene: vivid crimson (or ice on a crimson ground), a
   soft static glow. Static, not animated, so it costs one paint, not one
   per frame. */
.il .il-c{stroke:var(--il-accent);filter:drop-shadow(0 0 6px var(--il-accent-glow));}
.il .il-c.il-fill{fill:var(--il-accent);stroke:none;}
/* a few glass fills for depth, never more than a couple per scene */
.il .il-glass{stroke:none;fill:var(--il-fill);}
/* a generic solid fill in the line colour, for a small dot or seal */
.il .il-fill{fill:var(--il-line);stroke:none;}
.il .il-dim{opacity:.55;}
/* Stroke width would otherwise scale with the artwork's own size (usually
   what you want: a 480px scene reads a touch bolder than a 220px one). Ask
   for a fixed screen-pixel width only on the handful of parts that also
   get transform-scaled by a loop or a hover (a scaled stroke inside a
   scaled group would otherwise double-thicken); non-scaling-stroke keeps
   those crisp instead. */
.il-loop-breathe,.il-loop-ripple,.il-h-lift,.il-h-scale{vector-effect:non-scaling-stroke;}

/* ---------- 3. draw-in ----------
   Any path/shape with .il-d "draws in". Default (no JS, or JS has not run
   yet): fully drawn, per the hard rule that nothing may stay invisible if
   JS fails. illus-kit.js measures each .il-d with getTotalLength(), writes
   the result to --len, then adds .il-armed to the <svg class="il"> itself,
   only once that measurement exists, which is what actually hides the
   line ready to draw. .is-drawn (added per illustration, once, the first
   time its section enters view) plays the reveal, staggered by --il-order
   (DOM order among the illustration's own .il-d parts, or an explicit
   data-order="n" on the element). */
.il-d{stroke-dashoffset:0;}
.il.il-armed .il-d{
  stroke-dasharray:var(--len,300) var(--len,300);
  stroke-dashoffset:var(--len,300);
}
.il.il-armed.is-drawn .il-d{
  transition:stroke-dashoffset calc(900ms + var(--il-order,0) * 90ms) var(--ease-snap,cubic-bezier(.16,1,.3,1)) calc(var(--il-order,0) * 90ms);
  stroke-dashoffset:0;
}
@media (prefers-reduced-motion:reduce){
  .il.il-armed .il-d{stroke-dashoffset:0 !important;transition:none !important;}
}

/* ---------- 4. living loop ----------
   .il-loop marks the part; one of the seven motion classes below picks the
   pattern. The animation only ever exists under (prefers-reduced-motion:
   no-preference) AND while the illustration carries .is-live, which
   illus-kit.js adds only while it is on screen and the tab is visible, and
   removes the moment either stops being true. That means an off-screen or
   backgrounded loop is not merely paused, it never starts costing frames
   in the first place, and reduced motion never has to override anything:
   the base (non-media, non-live) rule for each class below IS its finished
   static look. */
.il-loop-pulse{stroke-dasharray:var(--il-pulse-size,16) 9999;stroke-dashoffset:calc(var(--len,300) * .5);opacity:.85;}
.il-loop-rotor{transform-box:fill-box;transform-origin:center;}
.il-loop-float{transform-box:fill-box;}
.il-loop-ripple{transform-box:fill-box;transform-origin:center;opacity:.35;}
.il-loop-breathe{transform-box:fill-box;transform-origin:center;}
.il-loop-blink{opacity:.7;}
.il-loop-page{transform-box:fill-box;transform-origin:0% 50%;}

@media (prefers-reduced-motion:no-preference){
  .il.is-live .il-loop-pulse{animation:il-pulse var(--il-pulse-t,2.6s) linear infinite;animation-delay:var(--il-d,0s);}
  .il.is-live .il-loop-rotor{animation:il-rotor var(--il-rotor-t,6s) linear infinite;}
  .il.is-live .il-loop-float{animation:il-float var(--il-float-t,4.4s) ease-in-out infinite;animation-delay:var(--il-d,0s);}
  .il.is-live .il-loop-ripple{animation:il-ripple var(--il-ripple-t,3.2s) cubic-bezier(.2,.6,.35,1) infinite;animation-delay:var(--il-d,0s);}
  .il.is-live .il-loop-breathe{animation:il-breathe var(--il-breathe-t,3.6s) ease-in-out infinite;animation-delay:var(--il-d,0s);}
  .il.is-live .il-loop-blink{animation:il-blink var(--il-blink-t,2.1s) ease-in-out infinite;animation-delay:var(--il-d,0s);}
  .il.is-live .il-loop-page{animation:il-page var(--il-page-t,5.5s) ease-in-out infinite;}
}
@keyframes il-pulse{
  0%{stroke-dashoffset:var(--len,300);opacity:0;}
  8%{opacity:1;}
  90%{opacity:1;}
  100%{stroke-dashoffset:calc(-1 * var(--il-pulse-size,16));opacity:0;}
}
@keyframes il-rotor{to{transform:rotate(360deg);}}
@keyframes il-float{0%,100%{transform:translateY(0);}50%{transform:translateY(calc(-1 * var(--il-float-y,5px)));}}
@keyframes il-ripple{0%{transform:scale(.55);opacity:0;}14%{opacity:.9;}70%{opacity:.3;}100%{transform:scale(1.4);opacity:0;}}
@keyframes il-breathe{0%,100%{transform:scale(1);}50%{transform:scale(1.03);}}
@keyframes il-blink{0%,100%{opacity:.3;}50%{opacity:1;}}
@keyframes il-page{0%,100%{transform:rotateY(0deg);}46%,54%{transform:rotateY(-22deg);}}

/* ---------- 5. hover: a response that means something ----------
   .il-host is whatever the pointer actually lands on (the card, the
   figure, or the <svg class="il"> itself if there is no separate wrapper).
   Pure CSS on purpose (:hover / :focus-within), so it works even with JS
   disabled and never needs a listener per illustration. Three ready-made
   helpers cover the common cases; a page builder is free to invent more of
   its own under the same il-h-* naming (il-h-valve, il-h-network, whatever
   the scene needs) by writing its own rule the same shape as these three.
   Cursor-only effects stay behind (pointer:fine); focus-within (keyboard)
   is never gated on pointer, a focus ring should feel immediate. */
.il-h-lift,.il-h-glow{transition:transform .35s var(--ease-snap,cubic-bezier(.16,1,.3,1)),filter .35s ease;}
/* BOX RULE (motion-kit.css section 6) + R10 restore (REQUIREMENTS Q8): the BOX that holds an illustration
   answers hover with its border ring only (no lift, no glow on the box). The PARTS of the illustration inside
   it react again, as in round 8: a part lifts (il-h-lift), glows (il-h-glow), scales (il-h-scale) and the
   topic's own animation speeds up (il-h-speed). These move SVG parts, never the card. */
@media (hover:hover) and (pointer:fine){
  .il-host:hover .il-h-lift{transform:translateY(calc(-1 * var(--il-lift,6px)));}
  .il-host:hover .il-h-glow{filter:drop-shadow(0 0 var(--il-glow-hover,13px) var(--il-accent-glow));}
  .il-host:hover .il-h-speed{animation-duration:var(--il-speed-hover,900ms) !important;}
  .il-host:hover .il-h-scale{transform:scale(var(--il-scale-hover,1.06));}
}
.il-host:focus-within .il-h-lift{transform:translateY(calc(-1 * var(--il-lift,6px)));}
.il-host:focus-within .il-h-glow{filter:drop-shadow(0 0 var(--il-glow-hover,13px) var(--il-accent-glow));}
.il-host:focus-within .il-h-speed{animation-duration:var(--il-speed-hover,900ms) !important;}
.il-host:focus-within .il-h-scale{transform:scale(var(--il-scale-hover,1.06));}
.il-h-scale{transition:transform .35s var(--ease-snap,cubic-bezier(.16,1,.3,1));transform-box:fill-box;transform-origin:center;}
@media (prefers-reduced-motion:reduce){
  .il-h-lift,.il-h-glow,.il-h-scale{transition:none;}
}

/* ---------- 6. sizing guardrail ----------
   220 to 480px is the useful range (spec F): under that a scene reads as
   an icon, over it the line weight and stroke count stop reading as one
   composed drawing. This is a ceiling, not a layout decision; a page
   builder still sizes the host element (figure, column, card). */
.il-frame{width:100%;max-width:480px;min-width:220px;margin-inline:auto;}

/* ---------- 7. print / no-motion-capable fallback ----------
   Printed illustrations show fully drawn, no dash, no loop trace left
   mid-frame. */
@media print{
  .il-d{stroke-dashoffset:0 !important;stroke-dasharray:none !important;}
  .il [class*="il-loop-"]{animation:none !important;}
}

/* ============================================================
   K4 SCENE KIT additions (DMITRY-REDESIGN-2026-09). Sections 1 to 7 above are
   K3's, untouched, every class in them still works exactly as documented.
   Sections 8 to 13 below add the mechanics six composed scene archetypes need
   on top of that: real icon vocabulary (Tabler stroke icons, Healthicons
   filled icons), a fill reveal and a hinge reveal for shapes .il-d cannot
   stroke-draw, two more hover helpers, a shared icon-tile look, and a data
   readout. Full contract and the six worked exemplars: ILLUSTRATION-GUIDE.md.
   ============================================================ */

/* ---------- 8. icon vocabulary (Tabler stroke icons + Healthicons filled icons) ----------
   Paste output from tools/icon.py show directly; it already wraps the icon in
   <g class="il-icon il-icon--stroke" ...> (Tabler, fill:none stroke:currentColor) or
   <g class="il-icon il-icon--fill" ...> (Healthicons, fill:currentColor stroke:none).
   Section 2's base primitive rule sets fill:none on every path, correct for a stroke
   icon but it would blank out a filled one, so filled icons need their own rule here.
   Colour follows the same --il-line / --il-accent switch as everything else in the
   kit: nothing to configure unless the icon IS the scene's one accent, in which case
   add .il-c to the same <g>, exactly like any other accent shape.
   Two different reveal mechanics, matching how each icon set is actually drawn (see
   icon.py's own docstring): a Tabler icon's paths are real strokes, add .il-d to EACH
   inner <path> yourself (the wrapping <g> has no measurable length, only a path, line,
   circle or rect does) and it draws in exactly like any hand-built line. A Healthicons
   path is a filled silhouette with no stroke to draw, add .il-fw (section 9) to the
   OUTER <g> instead, once, and the whole icon reveals with a scale-and-fade. Give every
   path of one icon the same data-order so it appears as one beat, not a sequence of
   internal strokes. */
.il .il-icon--stroke path{stroke:var(--il-line);}
.il .il-icon--fill path{fill:var(--il-line);stroke:none;}
.il .il-c.il-icon--stroke path{stroke:var(--il-accent);}
.il .il-c.il-icon--fill path{fill:var(--il-accent);stroke:none;}

/* ---------- 9. fill reveal (Healthicons, or any other filled, non-stroke shape) ----------
   The named version of the one-shot custom reveal the original guide taught by hand (a
   scale-and-fade keyed to the kit's own .is-drawn), now a real class so nobody writes it
   out again. Same contract as .il-d: the base rule is fully shown, never invisible
   without JS; .il-armed hides it ready to play; .il-armed.is-drawn reveals it; reduced
   motion always resolves to shown. */
.il-fw{transform-box:fill-box;transform-origin:center;transform:scale(1);opacity:1;}
.il.il-armed .il-fw{
  transform:scale(.55);opacity:0;
  transition:transform .6s var(--ease-snap,cubic-bezier(.16,1,.3,1)) calc(var(--il-order,0) * 90ms),
             opacity .5s ease calc(var(--il-order,0) * 90ms);
}
.il.il-armed.is-drawn .il-fw{transform:scale(1);opacity:1;}
@media (prefers-reduced-motion:reduce){
  .il.il-armed .il-fw{transform:scale(1) !important;opacity:1 !important;transition:none !important;}
}

/* ---------- 10. hinge reveal (a page or flap that opens, not a stroke draw) ----------
   Same contract as .il-fw, a different motion: scales open from an edge instead of
   fading in place. transform-origin takes a PIXEL point via --il-hinge-x / --il-hinge-y
   (not a percentage; the hinge is rarely the shape's own bounding-box corner, see the
   DOCUMENTS exemplar). This is the exact mechanism the original guide's document
   exemplar wrote out as page-only CSS, formalised into the kit since every DOCUMENTS
   scene on the site needs the identical pattern. */
.il-hinge{
  transform-box:view-box;transform-origin:var(--il-hinge-x,50%) var(--il-hinge-y,50%);
  transform:scaleY(1);opacity:1;
}
.il.il-armed .il-hinge{
  transform:scaleY(0);opacity:.3;
  transition:transform 1.05s var(--ease-snap,cubic-bezier(.16,1,.3,1)) calc(var(--il-order,0) * 90ms),
             opacity .8s ease calc(var(--il-order,0) * 90ms);
}
.il.il-armed.is-drawn .il-hinge{transform:scaleY(1);opacity:1;}
@media (prefers-reduced-motion:reduce){
  .il.il-armed .il-hinge{transform:scaleY(1) !important;opacity:1 !important;transition:none !important;}
}

/* ---------- 11. two more hover helpers (join il-h-lift / glow / speed / scale, section 5) ----------
   il-h-approach: the part moves toward a point that is not simply "up" (a HUB satellite
   drawing closer to the centre it orbits), direction and distance set per element with
   --il-approach-x / --il-approach-y since every satellite sits at a different angle.
   il-h-turn: a small rotation (a page corner turning, a dial nudging), angle via
   --il-turn, pivot via --il-turn-origin when it should not be the shape's own centre.
   (Restored in R10: these move parts of the illustration inside a box, never the box.) */
.il-h-approach{transition:transform .4s var(--ease-snap,cubic-bezier(.16,1,.3,1));}
@media (hover:hover) and (pointer:fine){
  .il-host:hover .il-h-approach{transform:translate(var(--il-approach-x,0),var(--il-approach-y,0));}
}
.il-host:focus-within .il-h-approach{transform:translate(var(--il-approach-x,0),var(--il-approach-y,0));}

.il-h-turn{
  transform-box:fill-box;transform-origin:var(--il-turn-origin,center);
  transition:transform .4s var(--ease-snap,cubic-bezier(.16,1,.3,1));
}
@media (hover:hover) and (pointer:fine){
  .il-host:hover .il-h-turn{transform:rotate(var(--il-turn,-8deg));}
}
.il-host:focus-within .il-h-turn{transform:rotate(var(--il-turn,-8deg));}
@media (prefers-reduced-motion:reduce){
  .il-h-approach,.il-h-turn{transition:none;}
}

/* ---------- 12. icon tile (HUB satellites/centre, PATH milestones, DOCUMENTS badge, DATA badge) ----------
   A small rounded-square holding one icon, the same two-layer pattern as the LVAD
   exemplar's pump housing: a plain .il-glass rect behind for the fill, this class added
   to that SAME rect for a static drop shadow (painted once, not animated, so it costs
   nothing per frame), then a separate .il-d rect on top, same position and radius, for
   the ring that draws in. */
.il .il-tile{filter:drop-shadow(0 5px 14px rgba(3,8,20,.24));}
/* Tiles are OPAQUE (orchestrator fix): a see-through tile let connector lines show through the
   icon. Paint connectors first in the markup and tiles after them, and the tile hides the line. */
.il .il-glass.il-tile{fill:var(--il-tile);}

/* ---------- 13. DATA readout (the number and label beside a ring, bars or a rising line) ----------
   HTML, not SVG, so it sits next to .il-frame inside the same .il-host rather than
   inside the svg. Colour follows the section's real ground class directly (the same
   list section 1 reads), since this text is outside the svg that would otherwise switch
   it automatically. Pair with the sitewide .count[data-target] counter (motion-kit.js
   already runs it sitewide; nothing to wire here). */
.il-data__readout{text-align:center;margin-top:.6rem;}
.il-data__num{display:block;font-family:var(--display,inherit);font-size:2.4rem;font-weight:700;line-height:1;color:var(--ink,#0B1B33);transition:color .35s ease;}
.il-data__label{display:block;margin-top:.3rem;font-size:.85rem;color:var(--ink-soft,#42506B);}
:is(.sec-dark,.on-dark,.phero--dark,.glass-1,.glass-2,.glass-3) .il-data__num{color:var(--ink-on-dark,#F4F7FB);}
:is(.sec-dark,.on-dark,.phero--dark,.glass-1,.glass-2,.glass-3) .il-data__label{color:rgba(244,247,251,.72);}
.il-host:hover .il-data__num,.il-host:focus-within .il-data__num{color:var(--crimson-vivid,#E23A46);}
@media (prefers-reduced-motion:reduce){.il-data__num{transition:none;}}

/* ---------- 14. STAGE (orchestrator, 2026-09-30): give every section scene real presence ----------
   Dev: "nothing interesting happening on the screen". A scene floating as thin lines in a big
   navy field reads as a diagram, not an illustration. Wrap a standalone scene in .il-stage:
   a lit glass stage with a bevel, a soft crimson light behind the subject, a faint dot grid that
   fades at the edges, and a slow orbit ring for ambient life. The svg inside scales with the
   stage, so tiles and icons grow with it (Tabler strokes stay crisp: non-scaling-stroke).
   Use it for any scene that is not already inside a card or panel that frames it.
     <div class="il-stage il-host"> <svg class="il" viewBox="..."> ... </svg> </div>
   Size it from the page layout: at least 360px wide on desktop, filling its half of the section. */
.il-stage{position:relative;isolation:isolate;display:grid;place-items:center;width:100%;
  min-height:clamp(260px,30vw,460px);padding:clamp(18px,3vw,44px);border-radius:26px;overflow:hidden;
  background:
    radial-gradient(58% 52% at 50% 46%,rgb(226 58 70 / .13),transparent 72%),
    linear-gradient(158deg,rgb(255 255 255 / .075),rgb(255 255 255 / .018) 58%,rgb(255 255 255 / .04));
  border:1px solid rgb(255 255 255 / .11);
  box-shadow:inset 0 1px 0 rgb(255 255 255 / .14),inset 0 -1px 0 rgb(0 0 0 / .28),0 34px 80px -44px rgb(0 0 0 / .7);}
.il-stage::before{content:'';position:absolute;inset:0;z-index:-1;pointer-events:none;opacity:.5;
  background-image:radial-gradient(rgb(255 255 255 / .10) 1px,transparent 1.3px);background-size:22px 22px;
  -webkit-mask-image:radial-gradient(72% 68% at 50% 50%,#000 35%,transparent 100%);
          mask-image:radial-gradient(72% 68% at 50% 50%,#000 35%,transparent 100%);}
/* (the slow dashed orbit ring that used to circle the scene is gone: a decorative line) */
.il-stage > .il,.il-stage .il-frame{width:100%;max-width:560px;}
/* the stage answers the cursor like every other box: its border ring (motion-kit.css section 6), nothing else */
/* on white sections: a white stage with a navy hairline and a faint crimson light */
:is(.sec-paper,.sec-paper-2) .il-stage{
  background:radial-gradient(58% 52% at 50% 46%,rgb(226 58 70 / .07),transparent 72%),linear-gradient(158deg,#FFFFFF,#F3F5F9);
  border-color:rgb(12 24 51 / .09);box-shadow:inset 0 1px 0 #fff,0 30px 64px -42px rgb(12 24 51 / .32);}
:is(.sec-paper,.sec-paper-2) .il-stage::before{background-image:radial-gradient(rgb(12 24 51 / .09) 1px,transparent 1.3px);}
/* bolder linework inside a stage: scenes read from across the room */
.il-stage .il{--il-sw:2.1;}
@media (max-width:640px){ .il-stage{min-height:220px;border-radius:20px;} }

/* ============================================================
   15. GLOSSY FILLED TILES (orchestrator, 2026-09-30, P2 GLOSSY TILE pass).
   Every .il-tile becomes a real app-icon chip instead of a thin outline square:
   a navy-glass gradient fill on dark grounds, a white-to-cool-grey gradient on
   light ones, a soft top gloss sheen, and the scene's ONE accent tile (auto-
   detected by illus-kit.js: the tile carrying the .il-c accent, else the
   biggest tile in the scene [a HUB centre], else the last tile in DOM order
   [a PATH's final milestone] -- a lone tile, a DATA or DOCUMENTS badge, is
   always the accent since there is nothing else to compare it to) painted in
   the vivid crimson gradient instead. Section 12 above still sets the flat
   --il-tile / --il-accent colour and the static drop shadow; this section only
   adds the gradient fill on top of it, so a scene illus-kit.js has not yet
   enhanced (no-JS, or before it runs) still paints that flat colour, never
   blank, and every rule below is additive, nothing here is removed.
   The gradient itself lives in an SVG <defs> illus-kit.js injects once per
   .il (unique ids per instance, so several scenes on one page never collide);
   its stops read the tokens below, so the SAME injected gradient repaints
   correctly if the ground class ever changes, no extra JS involved. */
.il{
  --il-tile-grad-a:#FFFFFF;--il-tile-grad-b:#E7EBF2;
  --il-accent-grad-a:#FF6B75;--il-accent-grad-b:#A01D2A;
}
:is(.sec-dark,.on-dark,.phero--dark,.glass-1,.glass-2,.glass-3) .il,
.il.il--dark,.il--dark .il{
  --il-tile-grad-a:#2C3F68;--il-tile-grad-b:#141F3A;
  --il-accent-grad-a:#FF6B75;--il-accent-grad-b:#A01D2A;
}
.il.il--light,.il--light .il{
  --il-tile-grad-a:#FFFFFF;--il-tile-grad-b:#E7EBF2;
  --il-accent-grad-a:#FF6B75;--il-accent-grad-b:#A01D2A;
}
/* crimson ground: the vivid crimson accent would vanish on a crimson tile, so
   (matching section 1's own reasoning for line/fill/accent) the accent tile
   flips to an ice-white gradient instead; the regular tile keeps a deeper
   crimson-on-crimson tint for depth without competing with it */
.sec-crimson .il{
  --il-tile-grad-a:#E0616B;--il-tile-grad-b:#9C1B27;
  --il-accent-grad-a:#FFFFFF;--il-accent-grad-b:#FFD3D6;
}
/* the gradient stops themselves, painted once per svg by illus-kit.js; class
   names only, no inline colour, so ground changes repaint them with no JS */
.il .il-tile-stop-a{stop-color:var(--il-tile-grad-a);}
.il .il-tile-stop-b{stop-color:var(--il-tile-grad-b);}
.il .il-accent-stop-a{stop-color:var(--il-accent-grad-a);}
.il .il-accent-stop-b{stop-color:var(--il-accent-grad-b);}

/* the fill itself: illus-kit.js points --il-tile-fill-url / --il-tile-accent-url
   at this svg's own injected gradient; no JS yet (or JS failed) falls back to
   the flat colour section 12 already set, so nothing is ever invisible */
.il .il-glass.il-tile{fill:var(--il-tile-fill-url, var(--il-tile));}
.il .il-glass.il-tile.il-tile--accent{fill:var(--il-tile-accent-url, var(--il-accent));}

/* the icon glyph on top of a FILLED tile: forced white so it reads like a
   coloured app icon (white glyph on a coloured chip), only where it actually
   needs it. A regular tile's icon already follows --il-line, which is already
   the right contrast colour for its own ground, so only the ONE accent tile's
   icon needs overriding -- specificity beats section 8's own .il-c rule, so an
   icon that is itself the scene's accent (a single badge tile, like the
   "societies" scene) does not go crimson-glyph-on-crimson-tile invisible. */
.il .il-tile--accent-icon.il-icon--stroke path,
.il .il-c.il-tile--accent-icon.il-icon--stroke path{stroke:#FFFFFF;}
.il .il-tile--accent-icon.il-icon--fill path,
.il .il-c.il-tile--accent-icon.il-icon--fill path{fill:#FFFFFF;stroke:none;}

/* top gloss sheen: a same-shape duplicate illus-kit.js paints just above the
   fill (below the ring and the icon), white fading out by half height -- the
   same trick that reads as "glossy" on SSF's app-icon tiles. Fill is set
   inline per instance (the gradient id is unique per svg); this only clears
   the stroke section 2's base primitive rule would otherwise give it. */
.il .il-tile-gloss{stroke:none;pointer-events:none;}
/* soft ambient halo behind the ONE accent tile only, opacity-animated on
   hover/focus per the site's compositor-only rule (never box-shadow in a
   loop): a blurred, oversized duplicate of the tile painted in the accent
   colour, sitting behind it, quiet at rest and brighter under a pointer. */
.il .il-tile-halo{stroke:none;pointer-events:none;opacity:.26;transition:opacity .4s ease;}
@media (prefers-reduced-motion:reduce){.il .il-tile-halo{transition:none;}}

/* coordinated pop + tilt: illus-kit.js groups a tile's fill, ring and icon
   into one .il-tile-unit (or reuses the scene's own wrapper when one already
   holds exactly that trio, like a HUB satellite's .il-h-approach) so the
   whole chip moves as one rigid piece on hover/focus, never just the flat
   colour square sliding out from under a static icon. transform-box:fill-box
   is required here (K4's own documented rule): without it the group would
   scale/rotate from the svg's own (0,0) origin, not its own centre. */
.il-tile-unit{
  transform-box:fill-box;transform-origin:center;
  transition:scale .4s var(--ease-snap,cubic-bezier(.16,1,.3,1)),rotate .4s var(--ease-snap,cubic-bezier(.16,1,.3,1));
}
@media (prefers-reduced-motion:reduce){.il-tile-unit{transition:none;}}

/* connectors and traces a touch bolder (spec: "about 2px") and brighter on
   hover. Scoped to direct-child paths only: a connector/trace is always a
   direct child of the <svg class="il"> root in every archetype (HUB, PATH,
   DATA's arc), while an icon glyph is always nested at least one level deep
   inside its own .il-icon group and a tile's ring is a rect/circle, not a
   path -- so this never thickens the crisp Tabler icon strokes or a tile's
   own ring. var(--il-sw,2) (not a bare 2px) means an element that already
   sets its own --il-sw locally (the DATA arc does, inline) keeps that value;
   only a connector with no local override picks up the new, bolder default. */
.il>path.il-d,.il>path.il-loop-pulse{stroke-width:var(--il-sw,2);}
.il>path.il-c{transition:filter .4s ease;}
@media (hover:hover) and (pointer:fine){
  .il-host:hover .il>path.il-c{filter:drop-shadow(0 0 10px var(--il-accent-glow));}
}
.il-host:focus-within .il>path.il-c{filter:drop-shadow(0 0 10px var(--il-accent-glow));}
@media (prefers-reduced-motion:reduce){.il>path.il-c{transition:none;}}

/* ============================================================
   16. BANNED ARCHETYPES (KB, 2026-09-30, Dev review row J4)
   ============================================================
   Dev: "why is every illustration connected with each other and shit like
   that, you gotta work on individual illustrations." The HUB archetype (a
   centre tile with satellite tiles joined by drawn connector lines, old
   guide section 9a) and the PATH archetype (milestone tiles joined by one
   drawn route, old guide section 9c) are BANNED for any new illustration
   from this date on. A scene with two or more separate icon tiles joined
   by a visible line reads as a diagram, not an illustration, and it is the
   single thing Dev flagged hardest in this review.
   Sections 8, 12 and 15 above (icon vocabulary, the plain icon tile, the
   glossy tile upgrade) are NOT removed and NOT rewritten: pages already
   live on this site still reference them, and rewriting page markup is out
   of scope for this file. They simply stop being the starting point for
   new work. Build individual objects instead, both below:
     TILE3D (17)  one small glossy dimensional icon chip, on its own or
                  inside a card. Sizes 64 to 176px.
     OBJECT (18)  one larger single-topic composition, 240 to 420px: a big
                  TILE3D or glossy disc holding a large glyph, plus exactly
                  one topic animation living ON or AROUND that one object.
                  Never a second object joined to it by a line.
   SSF reached this exact end state on its own About page (glossy, filled,
   dimensional "app-icon" tiles, one object per card, SSF handbook 33.11
   #10); section 17 recolours that proven recipe to navy / crimson / white.
   Full contract, worked exemplars and the exact markup: ILLUSTRATION-
   GUIDE.md, and Dmitry - Fable V3/_illus-lab.html.
   ============================================================ */

/* ============================================================
   17. TILE3D: one glossy, dimensional, individually-drawn icon chip
   ============================================================
   Recoloured from the pattern Dev approved on SSF (SSF Theme - Library V3
   snippets/ssf_about.liquid, .pillar-icon / .give-card__icon): a diagonal
   two-stop gradient body, an inner bevel (a top inner highlight, a bottom
   inner shade, a hairline inset rim), a top-left gloss highlight, a soft
   outer shadow plus a coloured ambient glow, and a white (or navy, on the
   pale variant) glyph with its own small depth shadow.

   Markup (copy this shape exactly; every class is documented below):

     <div class="il-host" tabindex="0">                 the hover/focus
       <div class="tile3d tile3d--crimson tile3d--96">  trigger is always
         <span class="tile3d__face">                    a DIFFERENT element
           <span class="tile3d__glint" aria-hidden="true"></span>
           <svg class="tile3d__glyph" viewBox="0 0 24 24" aria-hidden="true">
             <path d="..."/>                             a real Tabler or
           </svg>                                         Healthicons path,
         </span>                                           never freehand
       </div>
     </div>

   .il-host is the SAME hover/focus contract the rest of this kit already
   uses (section 6): whatever ancestor the pointer actually lands on, never
   the .tile3d itself, so a card, a figure, or this bare wrapper all work
   identically. Keeping host and tile as two different elements means one
   card can hold a tile PLUS a heading and a link (section 19) and every
   part answers the same hover together.

   Variants (colour only, pick exactly one): tile3d--crimson (the hero
   variant, a coral to deep crimson gradient; use it ONCE per group, on
   the one thing that matters most), tile3d--navy (navy glass, for every
   tile around the hero, or any tile sitting on the navy ground), tile3d--
   white (a pale tile with a navy glyph, for a tile that needs to sit on a
   white section without reading as a loud crimson mark).

   Sizes (pick one, or set --t3-size inline for anything in between or
   bigger, exactly how an OBJECT's own big tile does it, section 18):
   tile3d--64, tile3d--96 (the default), tile3d--128, tile3d--176.
   tile3d--disc makes any size a circle instead of a rounded square (the
   anatomical heart and the data object both use it).

   No JS required for any of this: colour, gloss, bevel and the resting
   glow are all plain CSS, visible immediately, every ground. JS (the K5
   runtime in illus-kit.js) only ever ADDS the one-shot arrival rise and
   the two optional ambient loops below (17e), both pure progressive
   enhancement; the base rule IS the finished look, per the site's own
   "nothing renders blank without JS" rule.
   ============================================================ */

/* 17a. sizes: each sets --t3-size, every other dimension below (radius,
   shadow spread, glyph size) is computed from it, so a value you set
   inline (an OBJECT's big tile, or any custom size) scales exactly the
   same way as the four presets. */
.tile3d{ --t3-size:96px; }
.tile3d--64{ --t3-size:64px; }
.tile3d--96{ --t3-size:96px; }
.tile3d--128{ --t3-size:128px; }
.tile3d--176{ --t3-size:176px; }

/* 17b. colour variants: three gradient stops for the body, one glow tint,
   one glyph colour. Crimson is the ONE hero colour per group, per the
   ban notice above. */
.tile3d,
.tile3d--navy{
  --t3-c1:#33477A; --t3-c2:#1B2C56; --t3-c3:#0C1833;
  --t3-glow:rgba(110,145,230,.45); --t3-glyph:#FFFFFF;
  --t3-glyph-shadow:rgba(3,8,20,.4);
}
.tile3d--crimson{
  --t3-c1:#FF8A91; --t3-c2:#E23A46; --t3-c3:#9C1B27;
  --t3-glow:rgba(226,58,70,.6); --t3-glyph:#FFFFFF;
  --t3-glyph-shadow:rgba(80,10,16,.45);
}
.tile3d--white{
  --t3-c1:#FFFFFF; --t3-c2:#F2F4F9; --t3-c3:#E3E8F1;
  --t3-glow:rgba(226,58,70,.28); --t3-glyph:#152342;
  --t3-glyph-shadow:rgba(21,35,66,.22);
}

/* 17c. the chip itself. Two nested elements on purpose: the OUTER .tile3d
   carries the transform (the pop, the tilt, the one-shot arrival) and the
   ambient glow, which must sit OUTSIDE the tile's own rounded corners; the
   INNER .tile3d__face is where overflow is actually clipped, holding the
   gradient body, the gloss, the glint sweep and the glyph. Nothing here is
   ever hidden without JS: this resting state already IS the finished tile. */
.tile3d{
  position:relative; isolation:isolate; flex:none; display:inline-block;
  width:var(--t3-size); height:var(--t3-size);
  border-radius:calc(var(--t3-size) * .25);
  transform:translateY(0) rotate(0deg) scale(1);
  transition:transform .5s var(--t3-spring,cubic-bezier(.34,1.56,.64,1));
}
.tile3d--disc{ border-radius:50%; }
.tile3d::after{
  content:''; position:absolute; z-index:-1; pointer-events:none;
  inset:calc(var(--t3-size) * -.16); border-radius:inherit;
  background:radial-gradient(circle, var(--t3-glow) 0%, transparent 70%);
  opacity:.22; transition:opacity .45s ease;
}
.tile3d__face{
  position:absolute; inset:0; border-radius:inherit; overflow:hidden; isolation:isolate;
  display:grid; place-items:center;
  background:linear-gradient(135deg, var(--t3-c1), var(--t3-c2) 55%, var(--t3-c3) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.5),
    inset 0 -12px 18px -9px rgba(3,8,20,.4),
    inset 0 0 0 1px rgba(255,255,255,.14),
    0 calc(var(--t3-size) * .13) calc(var(--t3-size) * .26) calc(var(--t3-size) * -.15) rgba(3,10,26,.5);
}
.tile3d--white .tile3d__face{
  box-shadow:
    inset 0 1px 0 #fff,
    inset 0 -10px 16px -9px rgba(12,24,51,.14),
    inset 0 0 0 1px rgba(12,24,51,.08),
    0 calc(var(--t3-size) * .13) calc(var(--t3-size) * .26) calc(var(--t3-size) * -.15) rgba(12,24,51,.28);
}
.tile3d__face::before{
  content:''; position:absolute; inset:0; pointer-events:none;
  background:radial-gradient(120% 90% at 24% 18%, rgba(255,255,255,.68), rgba(255,255,255,.14) 34%, transparent 62%);
}
.tile3d__glint{
  position:absolute; top:-45%; bottom:-45%; left:0; width:42%; pointer-events:none; opacity:0;
  background:linear-gradient(100deg, transparent, rgba(255,255,255,.65) 45%, transparent 82%);
  transform:translateX(-160%) skewX(-20deg);
}
.tile3d__glyph{
  position:relative; width:56%; height:56%; overflow:visible;
  color:var(--t3-glyph); fill:none; stroke:currentColor; stroke-width:1.8;
  stroke-linecap:round; stroke-linejoin:round;
  filter:drop-shadow(0 2px 3px var(--t3-glyph-shadow));
}
/* a Healthicons glyph (a filled silhouette, no stroke) pasted straight
   from icon.py: add this class to the same <svg> so it fills instead */
.tile3d__glyph--fill{
  fill:currentColor; stroke:none; width:60%; height:60%;
  filter:drop-shadow(0 -1px 0 rgba(255,255,255,.28)) drop-shadow(0 calc(var(--t3-size) * .03) calc(var(--t3-size) * .04) var(--t3-glyph-shadow));
}
/* GLYPH RULE (Dev review J, SSF 33.11 #10 "rich, filled, not thin line
   icons"): every tile glyph comes from a FILLED set, icon.py ids hf: and
   tf: (Healthicons filled, Tabler filled). An outline glyph is the last
   resort, only when no filled form exists, and then at stroke 2.4. */

/* 17d. hover and focus: none (2026-10-06, the box rule). The pop, tilt, glow-up and
   specular glint a tile used to get when its card was hovered are gone: the card
   answers with its own border ring (motion-kit.css section 6) and nothing else. */
@media (prefers-reduced-motion:reduce){
  .tile3d, .tile3d::after{ transition:none; }
}

/* 17e. entrance (rise and settle with a spring) and the two optional
   ambient loops, both progressive enhancement over the K5 runtime
   (illus-kit.js, appended below the existing runtime): the base CSS above
   (no JS, or before it runs) is always the fully settled, fully visible
   tile, never blank. JS adds .t3-armed only once it can actually run the
   reveal, which is what hides it ready to play; .is-in (added once, the
   first time the tile is seen) plays it; .is-live gates the ambient loop
   while the tile stays on screen and the tab stays visible. Stagger a row
   with --t3-order (an index you set inline, default 0). */
.tile3d.t3-armed{ opacity:0; transform:translateY(18px) scale(.8); transition:none; }
.tile3d.t3-armed.is-in{
  opacity:1; transform:translateY(0) rotate(0deg) scale(1);
  transition:
    opacity .5s ease calc(var(--t3-order,0) * 80ms),
    transform .8s var(--t3-spring,cubic-bezier(.34,1.56,.64,1)) calc(var(--t3-order,0) * 80ms);
}
@media (prefers-reduced-motion:reduce){
  .tile3d.t3-armed{ opacity:1 !important; transform:none !important; }
}
/* one gentle ambient loop, ONLY when the topic actually calls for it (add
   ONE of these two modifiers, never both on the same tile): tile3d--beat
   is a heartbeat scale on the glyph (a heart, a pulse reading); tile3d--
   rotor is a slow, steady turn (a pump, a gauge, a rotor). Both stop the
   instant the tile leaves the screen or the tab is hidden (.is-live), and
   neither exists at all under reduced motion. */
@media (prefers-reduced-motion:no-preference){
  .tile3d--beat.is-live .tile3d__glyph{ animation:t3-beat 1.8s ease-in-out infinite; transform-origin:center; }
  .tile3d--rotor.is-live .tile3d__glyph{ animation:t3-rotor 6s linear infinite; transform-origin:center; }
}
@keyframes t3-beat{
  0%,100%{ transform:scale(1); } 18%{ transform:scale(1.16); } 32%{ transform:scale(.97); }
  46%{ transform:scale(1.09); } 60%{ transform:scale(1); }
}
@keyframes t3-rotor{ to{ transform:rotate(360deg); } }

/* 17f. a labelled standalone tile (icon plus a caption underneath): the
   shape a specialty row or a catalogue actually needs, on any real page,
   not only the lab. The label's own colour is left to the page (it already
   knows its ground); this only sets layout and the focus ring. */
.tile3d-item{ display:flex; flex-direction:column; align-items:center; gap:.85rem; outline:none; border-radius:calc(var(--t3-size,96px) * .3 + 8px); }
.tile3d-item:focus-visible{ box-shadow:0 0 0 3px var(--crimson-glow,#FF6B75); }
.tile3d-item__label{ font-family:var(--display,inherit); font-weight:700; font-size:.92rem; line-height:1.3; text-align:center; max-width:15ch; color:inherit; }

/* 17g. a plain layout helper for a row of standalone tiles; optional,
   never required, every rule above works in any layout you already have. */
.tile3d-row{ display:flex; flex-wrap:wrap; gap:clamp(1.2rem,3vw,2.2rem); align-items:flex-start; justify-content:center; }

/* ============================================================
   18. OBJECT: one larger, single-topic composition (240 to 420px)
   ============================================================
   ONE rich object, never a diagram: a big TILE3D or glossy disc holding a
   large glyph, plus exactly ONE topic animation living ON or AROUND that
   single object. No second object, no connector line to anything else.

   Generic scaffold (every exemplar below builds on this same shape):

     <div class="obj3d il-host" style="--obj-size:320px" tabindex="0">
       <div class="tile3d tile3d--crimson tile3d--disc"
            style="--t3-size:clamp(220px,30vw,300px)">
         <span class="tile3d__face">
           <span class="tile3d__glint" aria-hidden="true"></span>
           <svg class="tile3d__glyph tile3d__glyph--fill" viewBox="0 0 48 48" aria-hidden="true"><path d="..."/></svg>
         </span>
       </div>
       <!-- exactly one topic animation goes here, see 18a to 18d -->
     </div>

   .obj3d carries .il-host itself (18 above already explains why host and
   tile are always different elements; here the OBJECT wrapper IS that
   host, and its inner .tile3d is the descendant every hover/focus rule in
   17d already answers, nothing new to write for that part). --t3-size
   takes clamp() (a real length) so the big tile shrinks on a narrow column
   without ever touching a percentage, which would break the box-shadow
   maths in 17c (box-shadow spread cannot be a percentage).

   Four worked exemplars, one per topic pattern in the brief, all live side
   by side in Dmitry - Fable V3/_illus-lab.html: the anatomical heart
   (18a), a speaking object (18b), a lesson/publication object (18c), and a
   data object (18d). Every one is exactly one object; none of them ever
   joins to a second one.
   ============================================================ */
/* aspect-ratio is load-bearing, not decoration: every absolutely positioned
   overlay inside an OBJECT (the ECG trace, the data ring) fills this box
   at inset:0 and assumes it is a true square, matching its own square
   viewBox. Without a fixed ratio the box would instead auto-size to its
   tallest IN-FLOW child (the big tile, which is ALLOWED to be smaller than
   --obj-size on purpose, 18a's whole point), squeezing that overlay's
   viewBox down to match and dragging every "lives at the edge" detail back
   in against the tile it was meant to clear. */
.obj3d{ position:relative; display:grid; place-items:center; width:100%; max-width:var(--obj-size,320px); aspect-ratio:1/1; margin-inline:auto; isolation:isolate; }
.obj3d:focus-visible{ outline:none; }
@media (max-width:640px){ .obj3d{ --obj-size:260px; } }

/* 18a. the ANATOMICAL HEART object: a large crimson glossy disc holding
   the Healthicons anatomical heart, a heartbeat on the glyph (tile3d--
   beat, 17e) and an ECG trace sweeping behind it. The heart's own solid
   fill hides the middle of the trace, so it only ever shows at the
   flanks, exactly like a real strip reads behind a chest overlay. This is
   the one place the older .il-d / .il-loop-pulse / .il-c primitives
   (sections 3 and 4 above) are still exactly right to reach for: give the
   trace svg BOTH classes, "obj3d__ecg il", so it inherits the whole
   ground-aware colour system for free, then draw ONE stroke living inside
   this one object. Never a line reaching for a second, separate tile;
   that is the exact thing section 16 bans. */
.obj3d--heart .obj3d__ecg{ position:absolute; inset:0; width:100%; height:100%; overflow:visible; }
@media (hover:hover) and (pointer:fine){
  .il-host:hover .obj3d--heart .il-loop-pulse{ animation-duration:1.3s; }
  .il-host:hover .obj3d--heart .tile3d--beat .tile3d__glyph{ animation-duration:.9s; }
}
.il-host:focus-visible .obj3d--heart .il-loop-pulse{ animation-duration:1.3s; }
.il-host:focus-visible .obj3d--heart .tile3d--beat .tile3d__glyph{ animation-duration:.9s; }

/* 18b. the SPEAKING object: a microphone tile with sound rings expanding
   from it, and only it, nothing across the way for them to reach. */
.obj3d__ring{
  position:absolute; left:50%; top:50%; width:58%; aspect-ratio:1/1; border-radius:50%;
  translate:-50% -50%; border:2px solid var(--crimson-glow,#FF6B75); opacity:0; pointer-events:none;
}
@media (prefers-reduced-motion:no-preference){
  .obj3d.is-live .obj3d__ring{ animation:obj3d-ring 2.8s var(--ease-snap,cubic-bezier(.16,1,.3,1)) infinite; animation-delay:var(--r-delay,0s); }
}
@keyframes obj3d-ring{
  0%{ transform:scale(.68); opacity:.5; } 30%{ opacity:.7; } 100%{ transform:scale(1.6); opacity:0; }
}
@media (hover:hover) and (pointer:fine){
  .il-host:hover .obj3d__ring{ animation-duration:1.5s; }
}
.il-host:focus-visible .obj3d__ring{ animation-duration:1.5s; }

/* 18c. the LESSON / PUBLICATION object: a small stack of pages, the top
   one turning, one badge tile naming what it is. Plain rounded rectangles
   stay freehand on purpose (a page outline is exactly the kind of simple
   geometric shape the medical accuracy sheet's own carve-out already
   allows, section 7); nothing here is anatomy or a person. */
.obj3d__docstack{ position:relative; width:64%; aspect-ratio:.76/1; perspective:1000px; }
.obj3d__page{
  position:absolute; inset:0; border-radius:calc(var(--obj-size,320px) * .045);
  background:linear-gradient(160deg,#FFFFFF,#EEF1F7);
  box-shadow:0 10px 24px -14px rgba(3,10,26,.4), inset 0 0 0 1px rgba(12,24,51,.06);
}
.obj3d__page--back2{ transform:translate(4%,6%) rotate(4deg); opacity:.7; }
.obj3d__page--back1{ transform:translate(-3%,3%) rotate(-3deg); opacity:.85; }
.obj3d__page--top{
  transform-style:preserve-3d; transform-origin:0% 50%; padding:15% 12% 12%;
  display:flex; flex-direction:column; gap:.55em;
}
.obj3d__page-line{ height:6%; min-height:4px; border-radius:3px; background:rgba(12,24,51,.14); }
.obj3d__page-line--short{ width:58%; }
@media (prefers-reduced-motion:no-preference){
  .obj3d.is-live .obj3d__page--top{ animation:obj3d-page 5.5s ease-in-out infinite; }
}
@keyframes obj3d-page{ 0%,100%{ transform:rotateY(0deg); } 48%,52%{ transform:rotateY(-18deg); } }
@media (hover:hover) and (pointer:fine){
  .il-host:hover .obj3d__page--top{ animation-duration:1.4s; }
}
.il-host:focus-visible .obj3d__page--top{ animation-duration:1.4s; }

/* 18d. the DATA object: a navy glass disc with a ring drawn to its exact
   fraction (never a full circle hidden behind a mask, the same rule the
   old DATA archetype already followed) and the counting number sitting
   INSIDE the same disc, not off to the side naming a second thing. Give
   the ring svg both "obj3d__ring-svg" and "il" for the same reason 18a's
   trace does: the ground-aware stroke colours come for free. Pair the
   number with the sitewide .count[data-target] counter, already running
   (motion-kit.js), nothing to wire for it here. */
.obj3d__ring-svg{ position:absolute; inset:0; width:100%; height:100%; overflow:visible; }
.obj3d__readout{ position:absolute; inset:0; display:grid; place-items:center; text-align:center; pointer-events:none; }
.obj3d__num{ display:block; font-family:var(--display,inherit); font-weight:700; font-size:clamp(1.5rem,5vw,2.4rem); line-height:1; color:#fff; }
.obj3d__label{ display:block; margin-top:.35rem; font-size:.76rem; color:rgba(244,247,251,.72); max-width:12ch; }

/* ============================================================
   19. CARD hover (2026-10-06): the box rule
   ============================================================
   Not a new class: this is a pointer, not a component. A card that holds an
   illustration answers hover and keyboard focus with its own 1.5px border
   ring and nothing else (motion-kit.css section 6): no lift, glow, wash,
   underline draw or sliding arrow, and no pop or tilt of a tile inside it.
   To drop a TILE3D into a real card, the card is the .il-host and the tile
   is any plain child of it, exactly the 17/18 contract above, and the card
   carries class box (or one of the kit card classes) for the ring.
   ============================================================ */
