/*
 * tait-widgets.css - authored stylesheet source (D-11).
 *
 * This sheet carries the load-bearing degradation gate, the accessible toggle
 * reset, and the widget's brand chrome. Motion is authored opt-in inside
 * @media (prefers-reduced-motion: no-preference) so the static baseline is the
 * default and an old WebView that does not understand the query gets a
 * functional instant toggle for free (RUNT-05, D-08). Every selector is scoped
 * under a tf- class so nothing leaks into the host page. build-inline-css.py
 * embeds this file into dist/tait-widgets.js as a string constant.
 *
 * ISOLATION MODEL (RUNT-03, RUNT-07; supersedes the 04-02 reading of D-12).
 * The widget ships inside a SCO rendered by Student Desktop, whose stylesheet is
 * the authoritative house layer, not an adversary. So typography and colour are
 * INHERITED from the host on purpose: font, size, weight, face and ink all come
 * from Student Desktop, and the widget adopts them. RUNT-07 asks for exactly
 * this - "reusing the house CSS layer/tokens rather than redefining them".
 *
 * What is isolated is only what is load-bearing:
 *   - the degradation gate (display), which a host override must never trap or
 *     force, so it stays !important;
 *   - box-sizing, which is layout correctness rather than aesthetics.
 * Everything else defers to the host. The brand survives as FORM (pill shape,
 * radius, spacing, >=44px target, motion), while COLOUR and TYPE come from the
 * host, so the widget reads native in whatever SCO it lands in.
 *
 * PROVISIONAL: nobody has yet rendered a SCO carrying this widget inside the
 * real LMS, so what Student Desktop actually supplies is unverified. Confirm
 * against a real embed before treating RUNT-07 as settled.
 */

/*
 * MASTER HOUSE TOKEN LAYER (D-13 full token foundation, D-16 faithful
 * translation of the imported TRAINFITNESS Interaction Style Guide image).
 * Adopted verbatim from the reference `@slice tokens` block
 * (runtime/reference/tf-interactions.css). This is the documented house
 * foundation every catalogue widget (Phases 8-12) will source from; only the
 * Reveal slice is exercised live this phase.
 *
 * These are INERT custom-property declarations only. Declaring them on :root
 * applies nothing on its own, so their placement here does NOT repaint the host
 * or the widget. Only CONSUMPTION on chrome would, and the Reveal deliberately
 * consumes only FORM tokens (spacing, radius, motion, hit target, outline
 * weight) - never a master COLOUR or TYPE token - so the inherit-first seam
 * (RUNT-07 / D-14) holds. See the ISOLATION MODEL header above.
 */
:root {
  /* ---- Palette (verbatim from image style guide §3) ---- */
  --tf-white:        #FFFFFF;
  --tf-ink:          #2B3A55; /* navy — text, outlines, leader lines */
  --tf-coral:        #C0473A; /* primary emphasis / interactive */
  --tf-coral-deep:   #B4342C; /* active / contracted */
  --tf-coral-soft:   #EC9C8C; /* skin / soft fills */
  --tf-cream:        #F4E4D4; /* bone cream */
  --tf-cream-deep:   #D4BC9C; /* bone shadow */
  --tf-blue:         #A4D4DC; /* silhouette / context */
  --tf-blue-light:   #B4DCE4;
  --tf-teal:         #5E8C9C; /* secondary cool accent */
  --tf-mauve:        #9C5C74; /* conceptual / mental-health */
  --tf-lavender:     #C4ACC4; /* conceptual secondary */
  --tf-sage:         #74A474; /* first-aid / safety */

  /* Derived pale surface tints (UI only — keep backgrounds calm) */
  --tf-cream-tint:   #FBF5EE;
  --tf-coral-tint:   #F8E7E2;
  --tf-blue-tint:    #ECF6F8;

  /* Navy alpha ramp — borders, rules, subtle fills */
  --tf-ink-70: rgba(43,58,85,.70);
  --tf-ink-55: rgba(43,58,85,.55);
  --tf-ink-16: rgba(43,58,85,.16);
  --tf-ink-10: rgba(43,58,85,.10);
  --tf-ink-06: rgba(43,58,85,.06);

  /* ---- Semantic roles ---- */
  --tf-bg:            var(--tf-white);
  --tf-text:          var(--tf-ink);
  --tf-muted:         var(--tf-ink-70);
  --tf-accent:        var(--tf-coral);
  --tf-accent-strong: var(--tf-coral-deep);
  --tf-surface:       var(--tf-cream-tint);
  --tf-surface-cool:  var(--tf-blue-tint);
  --tf-border:        var(--tf-ink-16);
  --tf-border-ink:    var(--tf-ink);
  --tf-focus:         var(--tf-coral-deep);

  /* ---- Reserved status trio (D-13, faithful to the image style guide) ----
     Defined-not-live: these three status tokens are declared here as part of the
     complete house layer, but no Reveal chrome consumes them this phase. They are
     reserved for the future catalogue widgets (Phases 8-12), where a state fill
     genuinely needs an error, success or warning colour. Declaring them on :root
     paints nothing on its own, so their presence here does not repaint the host
     or the Reveal. Values trace verbatim to the guide status trio. */
  --tf-status-error:   #D31334; /* red, destructive / error */
  --tf-status-success: #27B15D; /* green, success / correct */
  --tf-status-warning: #F4B20B; /* amber, warning / caution */

  /* ---- Typography (brand geometric sans, Gotham/Montserrat class) ---- */
  --tf-font: "Gotham", "Montserrat", "Century Gothic", "Avenir Next",
             system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --tf-fs-eyebrow: .75rem;   /* 12px */
  --tf-fs-label:   .875rem;  /* 14px */
  --tf-fs-body:    1rem;     /* 16px */
  --tf-fs-lead:    1.125rem; /* 18px */
  --tf-fs-h3:      1.375rem; /* 22px */
  --tf-fs-h2:      1.75rem;  /* 28px */
  --tf-lh-tight:   1.25;
  --tf-lh-body:    1.5; /* reconciled to the approved 06-UI-SPEC Body role, see base rule */
  --tf-ls-eyebrow: .12em;
  --tf-fw-regular: 400;
  --tf-fw-medium:  500;
  --tf-fw-semibold:600;
  --tf-fw-bold:    700;

  /* ---- Spacing (4px base) ---- */
  --tf-space-1: 4px;
  --tf-space-2: 8px;
  --tf-space-3: 12px;
  --tf-space-4: 16px;
  --tf-space-5: 24px;
  --tf-space-6: 32px;
  --tf-space-7: 48px;
  --tf-space-8: 64px;

  /* ---- Radii (pill = brand label shape) ---- */
  --tf-radius-sm:   6px;
  --tf-radius:      12px;
  --tf-radius-lg:   16px;
  --tf-radius-pill: 999px;

  /* ---- Outlines / weights ---- */
  --tf-outline-w:   2px; /* navy pill outline weight (label spec §10) */
  --tf-hairline:    1px;

  /* ---- Motion (calm & minimal) ---- */
  --tf-ease:      cubic-bezier(.2,.7,.3,1);
  --tf-ease-out:  cubic-bezier(.16,1,.3,1);
  --tf-dur-fast:  120ms;
  --tf-dur:       200ms;
  --tf-dur-slow:  320ms;
  --tf-dur-flip:  420ms; /* flip-card rotation only — slower than --tf-dur-slow so the on-device turn reads unhurried (G-08-1); the shared --tf-dur-slow stays 320ms for other/future consumers. */

  /* ---- Focus ring (high-contrast, always visible) ---- */
  --tf-ring-w:      3px;
  --tf-ring-offset: 2px;
  --tf-ring-color:  var(--tf-coral-deep);

  /* ---- Minimum touch target ---- */
  --tf-hit: 44px;
}

/*
 * BASE DOCUMENT TYPE AND COLOUR (D-14, the delivery mechanism for RUNT-07).
 * This is the one rule that sets the house brand on the Student Desktop HOST
 * document, so the runtime's existing inherit-first chrome lands on-brand by
 * inheritance rather than by pinning a master token onto the widget. Font, ink
 * colour, base size, weight and leading come from the master tokens above and
 * match the 06-UI-SPEC Typography Body role. When this sheet is published to the
 * Student Desktop global CSS, premise 2 (the house layer IS the TAIT visual
 * style guide) becomes true by construction, and the Reveal reads on-brand
 * through inheritance, not through a pinned colour or type token.
 *
 * SELECTOR CHOICE: the rule is wrapped in :where() so it scores zero specificity
 * (0,0,0). Any Student Desktop rule that already targets html or body, however
 * weakly, still wins on specificity, and even a bare element selector beats it on
 * the cascade, so this base brand is a floor the host can always override. This
 * is the T-06-09 mitigation: the base rule can set an unbranded host on-brand
 * but can never restyle a host that already brands itself. It applies nothing on
 * a page that never includes this sheet.
 *
 * LINE-HEIGHT RECONCILIATION: the imported reference had --tf-lh-body at 1.6, but
 * the approved 06-UI-SPEC Body role, the binding visual contract that governs the
 * shipped house layer, specifies 1.5. The token has no other consumer, so it is
 * reconciled to 1.5 above and consumed here, giving one unambiguous body leading
 * across the sheet. 1.5 traces to the approved 06-UI-SPEC (D-16, no invention).
 *
 * FONT-FAMILY FLAG (carried to 06-04): --tf-font is a self-declared geometric
 * sans stack. The exact host family must match Student Desktop's existing house
 * font, so confirm the live family at the 06-04 embed before locking this stack.
 */
:where(html) {
  font-family: var(--tf-font);
  color: var(--tf-text);       /* navy #2B3A55 ink, the house brand text colour */
  font-size: var(--tf-fs-body);/* 16px Body role */
  font-weight: var(--tf-fw-regular); /* 400 regular */
  line-height: var(--tf-lh-body);    /* 1.5, reconciled to 06-UI-SPEC Body role */
}

/*
 * Brand form tokens. Colour and font tokens are deliberately absent from the
 * Reveal chrome: they are the host's to supply (see ISOLATION MODEL above). The
 * calm-base palette (navy #2B3A55, white, cream #F4E4D4, pale blue #A4D4DC) and
 * the reserved status trio (#D31334 / #27B15D / #F4B20B) are now defined in the
 * master :root token block above as the complete house layer (D-13). The status
 * trio is defined-not-live: reserved for the catalogue widgets in Phases 8-12
 * and consumed by no Reveal chrome this phase (honest-partial per D-08 for the
 * unexercised tokens). A future widget that genuinely needs a status colour
 * sources it from the house layer above, not from this scoped block.
 */
.tf-reveal {
  /* Reveal-scoped FORM overrides. --tf-radius stays 10px here as a deliberate
     scoped override: the 06-UI-SPEC (binding visual contract, Spacing Scale)
     pins the Reveal corner radius at 10px, while the master :root --tf-radius
     is the 12px published house default for future widgets. --tf-pad now reads
     from the master 4px scale (--tf-space-4 == 16px exact), keeping the
     whitespace language while sourcing the value from the house layer. */
  /* D-13: this scoped override is the single genuine divergence from the house
     scale in the whole runtime. The 06-UI-SPEC Spacing Scale (binding visual
     contract) pins the Reveal corner radius at 10px, while the house :root
     default is 12px (00-core.css:127). A deliberate, audited divergence — it is
     declared here rather than left invisible. */
  /* tf-lint-allow: ui-spec-pin */
  --tf-radius: 10px;          /* rounded corners, soft language (UI-SPEC pin) */
  --tf-pad: var(--tf-space-4);/* 16px, generous padding, whitespace language */
}

/*
 * The combined degradation gate (D-13 hardened with the D-14 flag). Collapsible
 * content is hidden only when the runtime is ready AND that specific widget was
 * enhanced. A widget that fails to enhance never carries data-tf-enhanced, so it
 * stays fully visible (RUNT-06 fail-safe).
 */
html.tf-js-ready .tf-reveal[data-tf-enhanced] .tf-reveal__content {
  /* !important so a host override cannot trap collapsed content or force it
     open against the toggle state (T-04-05). */
  display: none !important;
}

html.tf-js-ready .tf-reveal[data-tf-enhanced].tf-reveal--open .tf-reveal__content {
  display: block !important;
}

/*
 * The only non-gate isolation: box-sizing. It does not inherit, so it is set
 * across the subtree explicitly, and it is !important with the root class
 * doubled to (0,2,0) so a host rule cannot flip the widget's internal box model
 * and break the layout. This is layout correctness, not aesthetics - typography
 * and colour are deliberately NOT reset here (see ISOLATION MODEL above).
 *
 * The doubling matters because a plain .tf-reveal * scores only (0,1,0) - the
 * universal selector contributes nothing - and would lose a tie to a host rule
 * on document order, since the host stylesheet can always follow the style tag
 * this runtime injects.
 */
.tf-reveal.tf-reveal,
.tf-reveal.tf-reveal * {
  box-sizing: border-box !important;
}

/*
 * The widget surface adopts the host's. No background and no colour of our own:
 * a pinned white surface would be unreadable if Student Desktop renders light
 * text on a dark ground, and a pinned ink would fight whatever the house layer
 * sets. Generous padding stays - spacing is form, and form is ours (D-02).
 */
.tf-reveal {
  padding: var(--tf-pad);
}

/*
 * The toggle is the pill motif (D-03, guide section 10): a real 2px border (not
 * a drop shadow), rounded corners, and the whole label row as the tap target, at
 * least 44px tall (WCAG 2.5.5).
 *
 * font: inherit is load-bearing, not cosmetic. A native <button> does NOT
 * inherit font-family or font-size from the page (Pitfall 1), so without this it
 * would drop to the UA default and be the one part of the widget that ignores
 * Student Desktop. RESEARCH offered "font: inherit (or the explicit brand
 * stack)"; the inherit fork is the one that satisfies RUNT-07's reuse mandate.
 *
 * currentColor makes the border and chevron track whatever ink the host sets, so
 * the chrome stays coherent with host type instead of fighting it.
 */
.tf-reveal__toggle {
  display: block;
  width: 100%;
  min-height: var(--tf-hit);
  text-align: left;
  cursor: pointer;
  background: transparent;
  color: inherit;
  font: inherit;
  border: var(--tf-outline-w) solid currentColor;
  border-radius: var(--tf-radius);
  margin: 0;
  padding: var(--tf-space-3) var(--tf-pad);
  text-transform: none; /* sentence case comes from the authored label copy */
}

/* Baseline focus ring (D-04). Plain :focus is the safe floor; :focus-visible is
   a Plan 03 progressive enhancement guarded behind @supports. currentColor so
   the ring stays visible against whatever ink the host supplies. */
.tf-reveal__toggle:focus {
  /* D-02: the 2px currentColor floor stays exactly as authored. Consuming
     --tf-ring-w/--tf-ring-color here would pin the reference coral onto chrome,
     violating the inherit-first isolation model reversed into place in Phase 4
     (25d7210). currentColor is the correct inherit-first choice here, not a
     shortcut, so the raw 2px is a declared exception rather than a missed token. */
  /* tf-lint-allow: focus-floor */
  outline: 2px solid currentColor;
  outline-offset: var(--tf-ring-offset);
}

/*
 * A CSS-drawn chevron affordance, no image or SVG asset fetch. A rotated square
 * border reads as a downward chevron at rest. A right margin (not flex gap, per
 * the Feature Floor) separates it from the label. currentColor tracks host ink.
 */
.tf-reveal__chevron {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-left: var(--tf-space-2);
  border-right: var(--tf-outline-w) solid currentColor;
  border-bottom: var(--tf-outline-w) solid currentColor;
  transform: rotate(45deg);
  vertical-align: middle;
}

/*
 * Motion (D-08, RUNT-05, RESEARCH Pattern 2 and Pattern 7). The static baseline
 * carries no transition. overflow hidden clips the collapsible content while
 * the runtime slides its measured height between 0 and scrollHeight. With no
 * runtime the content keeps its natural auto height, so nothing is clipped and
 * the degradation baseline is unaffected (RUNT-06).
 */
.tf-reveal__content {
  overflow: hidden;
}

/*
 * Motion is opt-in: the height and chevron transitions exist only when the
 * learner has not asked to reduce motion. A WebView that does not understand
 * the query never applies the transition, so the runtime height set lands
 * instantly (static baseline, safe direction). The JS reads this same cascade
 * back via getComputedStyle().transitionDuration, so when no transition is in
 * effect (reduce, or an engine without the query) it takes the instant path
 * and never waits on a transitionend that will not fire — the two layers can
 * never disagree.
 */
@media (prefers-reduced-motion: no-preference) {
  .tf-reveal__content {
    transition: height var(--tf-dur) ease;
  }

  .tf-reveal__chevron {
    transition: transform var(--tf-dur) ease;
  }
}

/*
 * Chevron rotation on open. The resting chevron points down; on open it rotates
 * a further half turn to point up, tracking the disclosure state (D-08). The
 * transform transition above animates this only when motion is allowed.
 */
.tf-reveal--open .tf-reveal__chevron {
  transform: rotate(225deg);
}

/*
 * 20-flipcard.css - the Flip Card widget chrome (CATA-01, 08-UI-SPEC LOCKED).
 *
 * Authored per the LOCKED UI-SPEC §"CSS gate + 3D form" block, NOT the
 * illustrative RESEARCH CSS. The flip card follows the Reveal isolation model
 * (inherit-first chrome, box-sizing the only non-gate isolation, motion
 * opt-in), but its concealment technique and presentation differ:
 *
 *   - EVERY flip presentation rule (grid stack, perspective, 3D rotate,
 *     back-face pre-rotation, concealment) is gated behind the enhanced
 *     compound `html.tf-js-ready .tf-flipcard[data-tf-enhanced]` (UI-SPEC
 *     Deviation D7). The un-enhanced baseline is plain readable block prose in
 *     source order (stem, front, back) - nothing pinned. An ungated grid stack
 *     would overlap the two prose faces and an ungated rotateY(180deg) would
 *     mirror the back prose, both breaking the JS-absent readable baseline
 *     (D-10, the "CSS present, JS absent" acceptance test).
 *   - Back-face at-rest concealment uses `visibility` (Pitfall 4): it is
 *     animation-compatible AND removes the back face from the a11y tree and tab
 *     order, unlike backface-visibility alone. NEVER display:none (D-10) and
 *     never a plain-class layout-removing technique.
 *   - Motion is OPT-IN (D-08): the state transform (rotateY) and the visibility
 *     swap are UNCONDITIONAL so under prefers-reduced-motion: reduce they apply
 *     INSTANTLY - that instant, no-rotation face swap IS the reduced-motion
 *     form. Only the `transition` declaration lives inside the
 *     @media (prefers-reduced-motion: no-preference) block.
 *
 * Chrome is inherit-first (04-D-14 / RUNT-07): colour and type inherit from the
 * host. The widget consumes FORM tokens only (--tf-space-5 face padding,
 * --tf-hairline + currentColor face border, --tf-radius-lg, --tf-dur-flip,
 * --tf-ease-out, --tf-hit, --tf-ring-offset) plus the documented
 * perspective:1200px form literal (no house token to reuse). No colour or type
 * token is pinned onto chrome; front/back differentiation comes from the flip
 * motion, the toggle state and position, never from a pinned face colour.
 *
 * The `-webkit-` prefixes on perspective/transform-style/backface-visibility
 * target the iOS 12 / Play-updated-Android WebView floor (Pitfall 3, carried
 * flag A3 - confirm on-device with the mobile team before locking the set).
 */

/*
 * Baseline (un-enhanced / runtime absent): plain readable block prose, source
 * order stem -> front -> back, both faces readable. Nothing is pinned here and
 * no rule removes a face from layout outside the enhanced gate.
 *
 * The only non-gate isolation is box-sizing: it does not inherit, so it is set
 * across the subtree explicitly, and it is !important with the root class
 * doubled to (0,2,0) so a host rule cannot flip the widget's internal box model.
 */
.tf-flipcard.tf-flipcard,
.tf-flipcard.tf-flipcard * {
  box-sizing: border-box !important;
}

/*
 * --- Everything below applies ONLY when the runtime is ready AND this specific
 * card was enhanced (data-tf-enhanced is written LAST, only after wiring). A
 * card that fails to enhance never carries the attribute, so it stays plain
 * readable prose (fail-safe, T-08-04). ---
 */

/* The enhanced root: perspective on the ANCESTOR (Pitfall 3) and the positioning
   context the absolutely-positioned toggle overlay anchors to. */
html.tf-js-ready .tf-flipcard[data-tf-enhanced] {
  position: relative;
  perspective: 1200px;
  -webkit-perspective: 1200px;
}

/* The inner is a single-cell grid stack so the card sizes to the tallest face
   with no JS height measurement and no absolute positioning, avoiding the
   zero-height collapse (Pitfall 2). preserve-3d keeps the rotated faces in 3D. */
html.tf-js-ready .tf-flipcard[data-tf-enhanced] .tf-flipcard__inner {
  display: grid;
  transform-style: preserve-3d;
  -webkit-transform-style: preserve-3d;
}

/* Both faces occupy the same grid cell (stacked, no absolute positioning). The
   hairline is currentColor (inherit-first, not a pinned --tf-border colour) so
   each face reads as a card without pinning a palette colour. */
html.tf-js-ready .tf-flipcard[data-tf-enhanced] .tf-flipcard__face {
  grid-area: 1 / 1;
  padding: var(--tf-space-5);
  border: var(--tf-hairline) solid currentColor;
  border-radius: var(--tf-radius-lg);
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

/* Pre-counter-rotate the back face so its prose reads correctly once the inner
   is flipped 180deg. */
html.tf-js-ready .tf-flipcard[data-tf-enhanced] .tf-flipcard__face--back {
  transform: rotateY(180deg);
}

/* Flip state - UNCONDITIONAL transform (Pitfall 5): under prefers-reduced-motion
   reduce it applies INSTANTLY, which is the no-rotation face swap. */
html.tf-js-ready .tf-flipcard[data-tf-enhanced].tf-flipcard--flipped .tf-flipcard__inner {
  transform: rotateY(180deg);
}

/*
 * At-rest concealment via VISIBILITY (Pitfall 4): animation-compatible AND
 * removes the back face from the a11y tree + tab order, unlike
 * backface-visibility alone. NEVER display:none (D-10 / 04-D-13). !important
 * mirrors Reveal's gate discipline so a host override cannot force a face
 * visible before the flip (T-08-05).
 */
html.tf-js-ready .tf-flipcard[data-tf-enhanced] .tf-flipcard__face--back {
  visibility: hidden !important;
}
html.tf-js-ready .tf-flipcard[data-tf-enhanced].tf-flipcard--flipped .tf-flipcard__face--back {
  visibility: visible !important;
}
html.tf-js-ready .tf-flipcard[data-tf-enhanced].tf-flipcard--flipped .tf-flipcard__face--front {
  visibility: hidden !important;
}

/*
 * Motion is OPT-IN ONLY: the transition is the ONLY thing gated by the media
 * query (D-08). A WebView that does not understand the query never applies the
 * transition, so the unconditional rotateY lands instantly - the safe direction.
 */
@media (prefers-reduced-motion: no-preference) {
  html.tf-js-ready .tf-flipcard[data-tf-enhanced] .tf-flipcard__inner {
    transition: transform var(--tf-dur-flip) var(--tf-ease-out);
  }

  /*
   * WR-01: keep the OUTGOING face visible while it rotates away so the animated
   * flip never blanks. The unconditional `visibility` swap above hid the
   * outgoing face INSTANTLY at flip-start, blanking the first half of the
   * rotation (front gone at t=0, back not yet crossed to face the viewer). Here
   * `visibility` still does the a11y concealment (tab order / AT tree) and
   * `backface-visibility` does the GEOMETRIC hiding during the turn; we only
   * defer the visibility HIDE to the end of the rotation while showing the
   * INCOMING face at once. Pattern: `transition: visibility 0s linear <delay>`
   * holds the property's old value for <delay> then switches, so the delay is
   * read from the destination state — the face becoming HIDDEN gets
   * delay=--tf-dur-flip (stays visible through the turn), the face becoming
   * VISIBLE gets delay 0 (present as it rotates in). This whole block is
   * media-gated, so under `prefers-reduced-motion: reduce` NONE of it applies and
   * the LOCKED instant no-rotation swap (D-08) is untouched.
   */
  html.tf-js-ready .tf-flipcard[data-tf-enhanced] .tf-flipcard__face {
    transition: visibility 0s linear 0s;
  }
  /* At rest the back is the hidden face: defer its hide so it stays visible while
     it rotates away on the unflip. */
  html.tf-js-ready .tf-flipcard[data-tf-enhanced] .tf-flipcard__face--back {
    transition-delay: var(--tf-dur-flip);
  }
  /* Flipped: the back is becoming visible — show it at once so it is present as it
     rotates in (backface-visibility hides it until it faces the viewer). */
  html.tf-js-ready .tf-flipcard[data-tf-enhanced].tf-flipcard--flipped .tf-flipcard__face--back {
    transition-delay: 0s;
  }
  /* Flipped: the front is becoming hidden — defer its hide to the end of the turn
     so it stays visible while it rotates away. */
  html.tf-js-ready .tf-flipcard[data-tf-enhanced].tf-flipcard--flipped .tf-flipcard__face--front {
    transition-delay: var(--tf-dur-flip);
  }
}

/*
 * The injected disclosure control overlays the faces so the ENTIRE card face is
 * the tap target (>=44px, WCAG 2.5.5). Safe to overlay because the faces are
 * prose-only with no focusable children (D-04). Inherit-first: font: inherit is
 * load-bearing (a native <button> does not inherit font), color: inherit and a
 * transparent background keep the control coherent with host ink.
 */
html.tf-js-ready .tf-flipcard[data-tf-enhanced] .tf-flipcard__toggle {
  position: absolute;
  inset: 0;
  width: 100%;
  min-height: var(--tf-hit);
  background: none;
  border: 0;
  margin: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

/* Baseline focus ring (Reveal precedent). currentColor so the ring stays visible
   against whatever ink the host supplies. */
html.tf-js-ready .tf-flipcard[data-tf-enhanced] .tf-flipcard__toggle:focus {
  /* D-02: the 2px currentColor floor stays exactly as authored. Consuming
     --tf-ring-w/--tf-ring-color here would pin the reference coral onto chrome,
     violating the inherit-first isolation model (Phase 4, 25d7210). */
  /* tf-lint-allow: focus-floor */
  outline: 2px solid currentColor;
  outline-offset: var(--tf-ring-offset);
}

/*
 * 30-tabs.css - the Tabbed Explainer widget chrome (CATA-02, 09-UI-SPEC LOCKED).
 *
 * Authored per the LOCKED 09-UI-SPEC "CSS Contract", NOT the illustrative
 * RESEARCH CSS and NOT the reference `@slice tabs` (which pins coral on the
 * selected underline and a muted colour on unselected tabs). Following the
 * Reveal/Flip Card isolation model:
 *   - box-sizing is the ONLY non-gate isolation (doubled root class + !important
 *     so a host rule cannot flip the widget's internal box model);
 *   - ALL concealment is gated behind the enhanced compound
 *     `html.tf-js-ready .tf-tabs[data-tf-enhanced]`, so a failed/absent runtime
 *     leaves every panel and heading readable in source order (T-09-05, the
 *     "CSS present, JS absent" degradation contract);
 *   - chrome is INHERIT-FIRST (04-D-14 / RUNT-07): `font: inherit` is
 *     load-bearing (a native <button> does not inherit font), colour tracks
 *     `currentColor`, and NO palette token (--tf-accent / --tf-muted) is pinned
 *     onto chrome. The selected-tab cue is a currentColor bar + a font-weight
 *     bump - two non-colour cues (WCAG 1.4.1);
 *   - motion is OPT-IN: the ONLY transition (the decorative underline scaleX)
 *     lives inside @media (prefers-reduced-motion: no-preference) over a static
 *     baseline, reusing --tf-dur (no new token). The panel swap is instant
 *     (native `hidden`); matchMedia is never read.
 *
 * The .tf-tabs__list strip and the .tf-tabs__tab buttons are BUILT by the runtime
 * (never emitted), so they exist only when enhanced and need no concealment gate
 * of their own; only the emitted headings and panels are gated below.
 */

/*
 * The only non-gate isolation: box-sizing. It does not inherit, so it is set
 * across the subtree explicitly, !important with the root class doubled to
 * (0,2,0) so a host rule cannot flip the widget's internal box model.
 */
.tf-tabs.tf-tabs,
.tf-tabs.tf-tabs * {
  box-sizing: border-box !important;
}

/*
 * --- Gated concealment. Applies ONLY when the runtime is ready AND this widget
 * was enhanced (data-tf-enhanced written LAST, only after wiring). A widget that
 * fails to enhance never carries the attribute, so every heading and panel stays
 * readable in source order (fail-safe, T-09-05). ---
 */

/* The standalone heading's text is rehosted into the tab button, so the live
   view hides the duplicate h3 - but only behind the gate, so the JS-absent
   fallback keeps a real <h3> above each panel. */
html.tf-js-ready .tf-tabs[data-tf-enhanced] .tf-tabs__heading {
  display: none;
}

/* Inactive panels are concealed via the native `hidden` attribute the enhancer
   toggles (APG-correct: removes the panel from the a11y tree and tab order).
   Only the enhancer ever sets `hidden`, so the JS-absent baseline never carries
   it - this belt-and-braces gated rule just guarantees the visual hide. */
html.tf-js-ready .tf-tabs[data-tf-enhanced] .tf-tabs__panel[hidden] {
  display: none;
}

/*
 * The horizontal-scroll strip (D-07). A single nowrap row that scrolls
 * horizontally with momentum when there are many or long labels; a persistent
 * full-width bottom hairline reads as a continuous rail, and an edge-peek
 * padding-inline-end reveals that more tabs are off-screen. currentColor rail,
 * never a pinned border colour.
 */
.tf-tabs__list {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--tf-space-1);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  padding-inline-end: var(--tf-space-4);
  border-bottom: var(--tf-hairline) solid currentColor;
}

/*
 * The tab control. flex:0 0 auto so a long label neither wraps nor ellipsis-
 * truncates (it is reached by scrolling). Inherit-first: `font: inherit` is
 * load-bearing, colour tracks host ink, semibold is the resting weight. >=44px
 * tap target (WCAG 2.5.5). position:relative anchors the ::after underline bar.
 */
.tf-tabs__tab {
  flex: 0 0 auto;
  scroll-snap-align: start;
  min-height: var(--tf-hit);
  padding: var(--tf-space-3) var(--tf-space-4);
  border: 0;
  background: none;
  font: inherit;
  font-weight: var(--tf-fw-semibold);
  color: inherit;
  cursor: pointer;
  position: relative;
  border-radius: var(--tf-radius-sm) var(--tf-radius-sm) 0 0;
}

/* Non-colour selection cue 1: the selected tab is bold. Unselected tabs stay
   full-strength currentColor (never dimmed with a pinned muted colour). */
.tf-tabs__tab[aria-selected="true"] {
  font-weight: var(--tf-fw-bold);
}

/*
 * Non-colour selection cue 2: a 3px currentColor underline bar present ONLY
 * under the selected tab (a shape/position cue). At rest scaleX(0); scaled to 1
 * when selected. background is currentColor - inherit-first, NEVER coral.
 */
.tf-tabs__tab::after {
  content: "";
  position: absolute;
  left: var(--tf-space-4);
  right: var(--tf-space-4);
  bottom: -1px;
  height: 3px;
  /* The 09-UI-SPEC selection-cue bar geometry. The two 3px radii match the
     sibling `height: 3px` on the line above so the bar's top corners round to
     exactly its own thickness; no house token carries this widget-specific
     value, and tokenising it is deferred (D-01 forbids shifting a computed
     pixel on a live-deployed widget). `height` is unpoliced by property under
     D-08, so only the border-radius needs excusing. */
  /* tf-lint-allow: selection-cue */
  border-radius: 3px 3px 0 0;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: center;
}

.tf-tabs__tab[aria-selected="true"]::after {
  transform: scaleX(1);
}

/*
 * Motion is OPT-IN ONLY: the underline scaleX transition is the sole motion and
 * the ONLY thing gated by the media query (Motion Contract). A WebView that does
 * not understand the query never applies the transition, so the bar snaps
 * instantly - the safe direction. Reuses --tf-dur (200ms); no new token.
 */
@media (prefers-reduced-motion: no-preference) {
  .tf-tabs__tab::after {
    transition: transform var(--tf-dur) var(--tf-ease-out);
  }
}

/*
 * Focus. Plain :focus is the safe floor (currentColor ring, always visible
 * against whatever ink the host supplies). :focus-visible is a @supports-guarded
 * progressive enhancement so keyboard users keep a high-contrast ring while
 * touch/pointer users do not get a lingering one.
 */
.tf-tabs__tab:focus {
  /* D-02: the 2px currentColor floor stays exactly as authored. Consuming
     --tf-ring-w/--tf-ring-color here would pin the reference coral onto chrome,
     violating the inherit-first isolation model (Phase 4, 25d7210). */
  /* tf-lint-allow: focus-floor */
  outline: 2px solid currentColor;
  outline-offset: var(--tf-ring-offset);
}

@supports selector(:focus-visible) {
  .tf-tabs__tab:focus {
    outline: none;
  }
  .tf-tabs__tab:focus-visible {
    /* D-17: --tf-ring-color resolves through --tf-coral-deep to a fixed brand
       hex, so this is a deliberate palette consumption on chrome. currentColor
       cannot guarantee WCAG focus contrast against an unknown host ground, so
       the shared ring colour is the sole sanctioned non-inherited hue on widget
       chrome. Excused here rather than by carving --tf-ring-* out of the palette
       set, so a future palette token added beside it is still caught. */
    /* tf-lint-allow: focus-visible-ring */
    outline: var(--tf-ring-w) solid var(--tf-ring-color);
    outline-offset: var(--tf-ring-offset);
  }
}

/*
 * forced-colors fallback (mirrors the reference @media (forced-colors) block):
 * the strip rail and the selected bar fall back to CanvasText, the focus ring to
 * Highlight, so the strip and the selection cue survive a forced-colours theme.
 */
@media (forced-colors: active) {
  .tf-tabs__list {
    border-bottom-color: CanvasText;
  }
  .tf-tabs__tab::after {
    background: CanvasText;
  }
  .tf-tabs__tab:focus,
  .tf-tabs__tab:focus-visible {
    outline-color: Highlight;
  }
}

/*
 * 40-steps.css - the Step-through Walkthrough widget chrome (CATA-03, 10-UI-SPEC LOCKED).
 *
 * Authored per the LOCKED 10-UI-SPEC "CSS Contract", following the
 * Reveal/Flip Card/Tabs isolation model:
 *   - box-sizing is the ONLY non-gate isolation (doubled root class + !important
 *     so a host rule cannot flip the widget's internal box model);
 *   - ALL concealment is gated behind the enhanced compound
 *     `html.tf-js-ready .tf-steps[data-tf-enhanced]`, so a failed/absent runtime
 *     leaves every step readable in source order inside the native <ol> (T-10-04,
 *     the "CSS present, JS absent" degradation contract);
 *   - chrome is INHERIT-FIRST (04-D-14 / RUNT-07): `font: inherit` is
 *     load-bearing (a native <button> and an injected <p> do not inherit font),
 *     colour tracks `currentColor`, and NO palette token (--tf-accent /
 *     --tf-muted) is pinned onto chrome. The disabled-end cue is opacity-only - a
 *     non-hue cue (WCAG 1.4.1);
 *   - motion is STATIC (D-09 LOCKED default): the step change is a synchronous
 *     native-`hidden` swap, so this sheet ships NO transition at all.
 *
 * DELIBERATE DIVERGENCE FROM TABS: there is NO `.tf-steps__heading { display: none }`
 * rule - the title <h3> stays visible inside the current step, because D-08 does
 * not repeat the title in the announce precisely because the heading is right
 * there (10-UI-SPEC Typography). The .tf-steps__nav, .tf-steps__prev/next and the
 * .tf-steps__status counter are BUILT by the runtime (never emitted), so they
 * exist only when enhanced and need no concealment gate of their own; the counter
 * is ALWAYS visible and is never gated or display:none'd (live-region Rule 4).
 */

/*
 * The only non-gate isolation: box-sizing. It does not inherit, so it is set
 * across the subtree explicitly, !important with the root class doubled to
 * (0,2,0) so a host rule cannot flip the widget's internal box model.
 */
.tf-steps.tf-steps,
.tf-steps.tf-steps * {
  box-sizing: border-box !important;
}

/*
 * --- Gated concealment. Applies ONLY when the runtime is ready AND this widget
 * was enhanced (data-tf-enhanced written LAST, only after wiring). A widget that
 * fails to enhance never carries the attribute, so every step stays readable in
 * source order (fail-safe, T-10-04). ---
 */

/* Non-current steps are concealed via the native `hidden` attribute the enhancer
   toggles (removes the <li> from the a11y tree and tab order). Only the enhancer
   ever sets `hidden`, so the JS-absent baseline never carries it - this
   belt-and-braces gated rule just guarantees the visual hide. */
html.tf-js-ready .tf-steps[data-tf-enhanced] .tf-steps__step[hidden] {
  display: none;
}

/*
 * The nav row: a flex row holding prev, the counter, next. Sits below the step
 * body separated by a top margin. currentColor throughout - no pinned palette.
 */
.tf-steps__nav {
  display: flex;
  align-items: center;
  gap: var(--tf-space-2);
  margin-top: var(--tf-space-5);
}

/* The live-region counter. margin:0 so it sits inline in the nav row; inherits
   host font (semibold) so it reads as a distinct chrome affordance without a
   pinned size. It is NEVER gated or hidden (a hidden region does not announce). */
.tf-steps__status {
  margin: 0;
  font-weight: var(--tf-fw-semibold);
}

/*
 * The prev/next controls. Two-axis 44px touch target (WR-02 fix: both min-height
 * AND min-width var(--tf-hit), finger-sized in both axes, WCAG 2.5.5).
 * Inherit-first: `font: inherit` is load-bearing, colour tracks host ink,
 * semibold is the resting weight. No pinned palette colour anywhere.
 */
.tf-steps__prev,
.tf-steps__next {
  min-height: var(--tf-hit);
  min-width: var(--tf-hit);
  padding: var(--tf-space-3) var(--tf-space-4);
  border: 0;
  background: none;
  font: inherit;
  font-weight: var(--tf-fw-semibold);
  color: inherit;
  cursor: pointer;
  border-radius: var(--tf-radius-sm);
}

/*
 * aria-disabled end styling (LOCKED, D-04, non-colour-safe). The control at a
 * sequence end is dimmed by OPACITY only (a non-hue cue that survives monochrome,
 * forced-colours and colour-blindness) plus cursor:default - NEVER a pinned muted
 * hue. The control stays in the DOM, stays focusable, and the JS handler enforces
 * the no-op; the aria-disabled attribute is the AT signal.
 */
.tf-steps__prev[aria-disabled="true"],
.tf-steps__next[aria-disabled="true"] {
  opacity: .45;
  cursor: default;
}

/*
 * Focus. Plain :focus is the safe floor (currentColor ring, always visible
 * against whatever ink the host supplies). :focus-visible is a @supports-guarded
 * progressive enhancement so keyboard users keep a high-contrast ring while
 * touch/pointer users do not get a lingering one.
 */
.tf-steps__prev:focus,
.tf-steps__next:focus {
  /* D-02: the 2px currentColor floor stays exactly as authored. Consuming
     --tf-ring-w/--tf-ring-color here would pin the reference coral onto chrome,
     violating the inherit-first isolation model (Phase 4, 25d7210). */
  /* tf-lint-allow: focus-floor */
  outline: 2px solid currentColor;
  outline-offset: var(--tf-ring-offset);
}

@supports selector(:focus-visible) {
  .tf-steps__prev:focus,
  .tf-steps__next:focus {
    outline: none;
  }
  .tf-steps__prev:focus-visible,
  .tf-steps__next:focus-visible {
    /* D-17: --tf-ring-color resolves through --tf-coral-deep to a fixed brand
       hex, so this is a deliberate palette consumption on chrome. currentColor
       cannot guarantee WCAG focus contrast against an unknown host ground, so
       the shared ring colour is the sole sanctioned non-inherited hue on widget
       chrome. Excused here rather than by carving --tf-ring-* out of the palette
       set, so a future palette token added beside it is still caught. */
    /* tf-lint-allow: focus-visible-ring */
    outline: var(--tf-ring-w) solid var(--tf-ring-color);
    outline-offset: var(--tf-ring-offset);
  }
}

/*
 * forced-colors fallback (mirrors the Tabs @media (forced-colors) block): the
 * focus ring falls back to Highlight so keyboard focus survives a forced-colours
 * theme. Under forced colours the opacity dim still reads and the aria-disabled
 * state is exposed to AT regardless.
 */
@media (forced-colors: active) {
  .tf-steps__prev:focus,
  .tf-steps__next:focus,
  .tf-steps__prev:focus-visible,
  .tf-steps__next:focus-visible {
    outline-color: Highlight;
  }
}

/*
 * 50-diagram.css - the Animated CSS/SVG Diagram widget chrome (CATA-04, 11-UI-SPEC LOCKED).
 *
 * Authored per the LOCKED 11-UI-SPEC, following the Reveal/Flip Card/Tabs/Steps
 * isolation model:
 *   - box-sizing is the ONLY non-gate isolation (doubled root class + !important
 *     so a host rule cannot flip the widget's internal box model);
 *   - chrome is INHERIT-FIRST (04-D-14 / RUNT-07): `font: inherit` is load-bearing
 *     on the Animate <button> and on the rehosted image-mode label toggle (a
 *     native button does not inherit font), colour tracks `currentColor`, and NO
 *     palette token (--tf-accent / --tf-muted / --tf-white / --tf-ink-16) is
 *     pinned onto chrome OR onto any SVG shape
 *     (MEMORY: inherit-first-vs-pinned-palette). Emphasis is weight/opacity/
 *     currentColor only.
 *
 *     THE RULE HAS EXACTLY ONE STATED, SCOPED EXCEPTION SINCE v3.1 PHASE 22.1,
 *     and it is recorded HERE, where the rule is stated, rather than only where
 *     it is taken (D-22). An image-mode diagram paints REGION MARKS over an
 *     approved illustration, and the halo half of those marks consumes a pinned
 *     light value. THE REASON, written down rather than implied: `currentColor`
 *     is a contract with the HOST SURFACE, and an approved illustration is NOT
 *     the host surface - it is content the host does not control, so a hue
 *     collision with it cannot be fixed by any amount of opacity tuning
 *     (measured in Phase 22: `currentColor` resolved to rgb(43, 58, 85) on both
 *     engines, invisible over dark navy artwork).
 *
 *     THE EXCEPTION'S DOMAIN IS BEING STATED PRECISELY, NOT RELAXED. It applies
 *     ONLY to a mark drawn over an illustration and NEVER to any chrome on the
 *     host surface: the caption, the description, the Animate control and the
 *     label toggle below are all still inherit-first with no palette token
 *     anywhere. The exception is DECLARED ONCE, in `60-hotspot.css` beside the
 *     halo rule it serves, as the scoped `--tf-region-halo` alias; this sheet
 *     shares that selector rather than carrying a second copy of the value,
 *     because two declarations of one pinned colour is two places for it to
 *     drift;
 *   - every SVG element is styled by TYPE via currentColor (per the 11-UI-SPEC
 *     "Per-shape SVG default treatment" table), stroke-width 2 in SVG user units
 *     (tuned to the 0 0 100 100 viewBox the enhancer sets);
 *   - motion is opt-in over a STATIC end-state baseline (D-02): the svg loads at
 *     its static end-state with NO transition; the opacity+scale draw-in exists
 *     ONLY inside @media (prefers-reduced-motion: no-preference), and the Animate
 *     control is HIDDEN under prefers-reduced-motion: reduce (D-04).
 *
 * DEGRADATION: the diagram is the FIRST widget whose live body is INJECTED, not
 * concealed - the inert markup carries NO <svg>, only the readable caption +
 * description. So it needs NO `.tf-js-ready` concealment gate for its content
 * (nothing to hide). The description is NEVER `display:none` when enhanced: it is
 * the aria-describedby target and must stay in the a11y tree (Pitfall 3). The
 * `.tf-js-ready` / `[data-tf-enhanced]` compound is used only to scope the
 * injected-svg motion + shape paint to a truly-enhanced widget.
 */

/*
 * The only non-gate isolation: box-sizing. It does not inherit, so it is set
 * across the subtree explicitly, !important with the root class doubled to
 * (0,2,0) so a host rule cannot flip the widget's internal box model.
 */
.tf-diagram.tf-diagram,
.tf-diagram.tf-diagram * {
  box-sizing: border-box !important;
}

/*
 * The figure surface adopts the host's (inherit-first): no background and no
 * colour of our own. Generous outer padding stays - spacing is form, and form is
 * ours (matches the reference @slice diagram outer padding).
 */
.tf-diagram {
  padding: var(--tf-space-6);
}

/*
 * The caption: short title, semibold so it reads as a distinct affordance without
 * a pinned colour. Sits below the injected svg, separated by a top margin.
 * Label-sized on purpose (11-UI-SPEC Typography - the caption is the largest
 * chrome text and it is label-sized).
 */
.tf-diagram__caption {
  margin-top: var(--tf-space-4);
  font-size: var(--tf-fs-label);
  font-weight: var(--tf-fw-semibold);
  line-height: var(--tf-lh-tight);
}

/*
 * The description: the JS-absent fallback prose AND the SVG aria-describedby
 * target. Inherit-first body role (size/weight/leading come from the host); only
 * the paragraph rhythm is declared. NEVER concealed when enhanced (Pitfall 3).
 */
.tf-diagram__description {
  margin-top: var(--tf-space-4);
}

/*
 * --- Per-shape SVG treatment (11-UI-SPEC "Per-shape SVG default treatment").
 * Scoped to the enhanced figure's injected svg and, since v3.0 Phase 22, to
 * DIRECT-child shapes only - the same form 60-hotspot.css:82-116 has always
 * carried, and for the same reason. Shapes carry GEOMETRY ONLY (the emitter
 * emits no fill/stroke); paint comes entirely from here via currentColor
 * (inherited host ink), NO pinned palette. stroke-width 2 is SVG user units,
 * tuned to the 0 0 100 100 canvas the enhancer sets.
 *
 * WHY THE NARROWING IS SAFE. Every author primitive arrives through the shared
 * inject helper, which appends it DIRECTLY to the svg root inside one loop with
 * no grouping (00-core.js::injectSvg). So every author shape on a diagram
 * already IS a direct child and the matched set for existing content is
 * unchanged by the `>`.
 *
 * WHY IT IS NECESSARY. The runtime-owned marker the image arm now paints is a
 * <g class="tf-hotspot__marker"> holding a <circle class="tf-hotspot__badge">
 * and a <text class="tf-hotspot__badge-num">, so under the descendant form those
 * two nodes were matched by the blocks below at (0,2,2) - beating their OWN
 * class selectors at (0,1,0). The measured consequence, named rather than
 * gestured at: the closed badge rendered as a SOLID currentColor disc instead of
 * an outlined one, and its number took `font-size: 4px` instead of 11, so the
 * digit was near-invisible against the disc containing it. The open state was
 * unaffected, because its tint selector is (0,3,0) - which would have left the
 * two states styled by different sheets. No existing test caught it because no
 * existing test put a badge inside a diagram (RESEARCH C-4).
 *
 * The three svg-TERMINAL rules further down (the size cap, the drawing
 * from-state and the motion block) select the svg ITSELF rather than its
 * contents and are deliberately untouched. ---
 */

/* Boxes and outline containers read as outlines: no fill, currentColor stroke. */
.tf-diagram[data-tf-enhanced] svg > rect,
.tf-diagram[data-tf-enhanced] svg > ellipse {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

/* Connectors / arrows: currentColor stroke. */
.tf-diagram[data-tf-enhanced] svg > line {
  stroke: currentColor;
  stroke-width: 2;
}

/* Nodes and arrowheads read as solid marks: currentColor fill, no stroke. */
.tf-diagram[data-tf-enhanced] svg > circle,
.tf-diagram[data-tf-enhanced] svg > polygon {
  fill: currentColor;
  stroke: none;
}

/* Labels: currentColor fill, host face (font-family inherit so labels read in
   the inherited brand type). */
.tf-diagram[data-tf-enhanced] svg > text {
  fill: currentColor;
  font-family: inherit;
  /*
   * font-size in SVG USER UNITS, tuned to the 0 0 100 100 canvas - the same
   * convention `stroke-width: 2` above is already documented under, and the
   * reason it must be declared here rather than left to inherit (UAT G-17-1,
   * second defect). An inherited px font-size is REINTERPRETED as user units
   * inside the viewBox, so the host's 16px became 16 units - 16% of the canvas -
   * and scaled with the box: the "Shaft" label measured 90px tall and ~36 units
   * wide, running from x=8 into the shaft rect at x=40. font-size is deliberately
   * absent from the emitter's SVG_ALLOWED allowlist (00-core.js: "ALL paint is
   * CSS ... never an authored attribute"), so this sheet is the only correct
   * layer to set it.
   *
   * 4 units keeps a 5-character label inside x=8..19, clear of the shaft at
   * x=40, and renders ~17px at the 420px cap - body-sized, as a diagram label
   * should be. It scales WITH the drawing below the cap, which is the point:
   * label and geometry stay in proportion at every width.
   */
  font-size: 4px;
}

/*
 * --- Motion: one-shot draw-in over a static end-state baseline (D-02). ---
 * The static end-state (opacity 1 / scale 1) is the DEFAULT the svg loads at, so
 * this sheet ships NO transition outside the no-preference block. The
 * tf-diagram--drawing FROM state (opacity 0 / scale .98) is set transiently by
 * the enhancer's draw() and cleared by settle(). The lg top margin separates the
 * svg from the Animate control row below it.
 */
.tf-diagram[data-tf-enhanced] svg {
  /*
   * Size cap (STYLE-CONFORMANCE, UAT G-17-1). The enhancer injects the svg with
   * width="100%" and viewBox="0 0 100 100" and no height, so WITHOUT a cap the
   * box becomes a square as wide as its container - measured at 806x806 in a
   * 966px SCO column on three engines. Every length inside is in user units of
   * that 100-unit canvas, so an uncapped box scales stroke-width 2 and the
   * text font-size with it: the "Shaft" label rendered ~113px tall and
   * overflowed the illustration.
   *
   * `width: min(100%, 420px); height: auto` is the reference's own declaration
   * for this element (reference/tf-interactions.css `.tf-diagram svg`, @slice
   * diagram) - adopted verbatim rather than invented here, which is the whole
   * point of a conformance phase. `height: auto` lets the viewBox hold the
   * aspect, so the box is 420x420 at the cap and shrinks square below it.
   * margin-inline auto centres it, because the figure is not a grid in this
   * sheet (the reference centres via `place-items` on a grid figure; this sheet
   * keeps the figure inherit-first and pays for the centring at the child).
   */
  width: min(100%, 420px);
  height: auto;
  margin-inline: auto;
  margin-bottom: var(--tf-space-5);
}

/* The FROM state while drawing. Present regardless of the motion preference (so
   the enhancer's class-toggle has a defined from/to), but it only TRANSITIONS
   when motion is allowed (block below); under reduce transitionMs() returns 0 and
   settle() clears it immediately, so the from-state is imperceptible. */
html.tf-js-ready .tf-diagram[data-tf-enhanced] svg.tf-diagram--drawing {
  opacity: 0;
  transform: scale(.98);
  transform-origin: center;
  /*
   * The from-state must be reached INSTANTLY, never transitioned into (G-17-2).
   * The @media (no-preference) block below transitions this svg
   * unconditionally, so without this line adding the class animated TOWARDS
   * opacity 0 over --tf-dur-slow instead of snapping there. draw() then flushed
   * a starting value of ~1 and the class removal transitioned 1 -> 1: a
   * correctly-timed transition between two identical values, i.e. nothing
   * visible. Snapping here is what gives the return leg something to animate
   * FROM.
   *
   * Specificity (0,4,2) beats the media block's (0,3,2), so this wins on
   * specificity and does not depend on source order.
   *
   * It does NOT suppress the draw-in. On class REMOVAL the element stops
   * matching this rule, so the new state carries the full transition and
   * animates 0 -> 1 - that IS the draw-in. settle() reads transitionMs(svg)
   * after the removal, so it still sees the full duration and the +50ms
   * anti-strand backstop is untouched.
   */
  transition: none;
}

/*
 * Motion is opt-in: the opacity+transform transition exists only when the learner
 * has not asked to reduce motion. A WebView that does not understand the query
 * never applies the transition, so the class-toggle lands instantly (static
 * baseline, safe direction). The JS reads this same cascade back via
 * getComputedStyle().transitionDuration (transitionMs), so when no transition is
 * in effect it takes the instant path and never waits on a transitionend that
 * will not fire - the two layers can never disagree.
 */
@media (prefers-reduced-motion: no-preference) {
  html.tf-js-ready .tf-diagram[data-tf-enhanced] svg {
    transition: opacity var(--tf-dur-slow) var(--tf-ease-out),
                transform var(--tf-dur-slow) var(--tf-ease-out);
  }
}

/*
 * --- The Animate control. Built by the runtime (never emitted), so it exists
 * only when enhanced and needs no concealment gate of its own. Inherit-first:
 * `font: inherit` is load-bearing, colour tracks host ink, semibold is the
 * resting weight, NO pinned palette. Two-axis 44px touch target (WCAG 2.5.5). ---
 */
.tf-diagram__animate {
  min-height: var(--tf-hit);
  min-width: var(--tf-hit);
  padding: var(--tf-space-3) var(--tf-space-4);
  border: 0;
  background: none;
  font: inherit;
  font-weight: var(--tf-fw-semibold);
  color: inherit;
  cursor: pointer;
  border-radius: var(--tf-radius-sm);
}

/*
 * --- The image-mode label toggle (v3.1 Phase 22.1, D-20/D-22.1-02). Built by the
 * runtime from the authored `li.tf-diagram__label`, so it exists only when
 * enhanced and needs no concealment gate of its own; a runtime-absent learner
 * reads the bare <li> text out of the emitted <ol>, untouched.
 *
 * Inherit-first exactly as the Animate control beside it: `font: inherit` is
 * LOAD-BEARING (a native <button> does not inherit font), colour tracks host ink,
 * NO pinned palette. This is the DIAGRAM'S PRIMARY DIRECTION - tapping the list
 * is the gesture this widget leads with - so the whole label row is the tap
 * target and it clears the 44px floor on the height axis (WCAG 2.5.5).
 *
 * THE OPEN-STATE CHROME IS DECLARED, ONCE, IN 60-hotspot.css. This paragraph used
 * to refuse it - it argued that the diagram's label discloses nothing of its own,
 * that what changes on activation is the HALO ON THE PICTURE (D-19/D-21), and
 * that a second competing visual cue on the list would put the learner's eye in
 * two places at once. THAT IS SUPERSEDED BY UAT G-22.1-2 and is rewritten here
 * rather than left standing beside a rule that contradicts it, per this file's
 * own convention.
 *
 * What overruled it: an operator looking at a real approved illustration opened a
 * region and could not tell from the list which label was showing. And the
 * reasoning had the direction backwards for THIS arm - the diagram is the
 * label-DRIVEN widget, so the list is the control the learner is acting on, which
 * gives it the STRONGER claim to feedback rather than the weaker one. A control
 * that reports nothing when it is operated is not a second cue competing with the
 * picture; it is the first cue missing.
 *
 * The treatment - a reserved `border-left` that gains `currentColor` plus an
 * underline on `aria-expanded="true"` - is declared in 60-hotspot.css beside the
 * shared region-mark rule, covering BOTH image-mode arms from one site so the two
 * lists cannot drift apart. `aria-expanded` on this button still carries the
 * state for assistive technology; it now carries it for sighted learners too.
 * The rule is scoped under `[data-tf-image]`, so a shapes-mode diagram label
 * gains nothing.
 *
 * IT DELIBERATELY DOES NOT CARRY `display: block; width: 100%`, which is what the
 * sibling `.tf-hotspot__toggle` uses to make its whole label row the tap target.
 * `width: 100%` is a RAW LITERAL on a property `tests/test_style_tokens.py` leaves
 * unpoliced-but-undecided (its `_KNOWN_UNDECIDED_ESCAPES` pin counts exactly the
 * eleven that already ship), and the shipped repair for a new one is a D-08
 * amendment naming the property - never a quiet twelfth. The button therefore
 * shrink-wraps its label and takes its floor from `min-height` plus the padding
 * either side, which clears 44px on both axes for every label this grammar can
 * carry. If a full-row target is later wanted here, it is a D-08 amendment, not a
 * one-line copy from the hot spot. ---
 */
.tf-diagram__toggle {
  min-height: var(--tf-hit);
  cursor: pointer;
  background: transparent;
  color: inherit;
  font: inherit;
  border: 0;
  margin: 0;
  padding: var(--tf-space-3) var(--tf-space-4);
}

/*
 * Focus. Plain :focus is the safe floor (currentColor ring, always visible
 * against whatever ink the host supplies). :focus-visible is a @supports-guarded
 * progressive enhancement so keyboard users keep a high-contrast ring while
 * touch/pointer users do not get a lingering one. Mirrors 40-steps.css exactly.
 *
 * The image-mode region mark's own focus ring is NOT declared here: it rides the
 * unscoped `.tf-hotspot__region-mark:focus` rules in 60-hotspot.css, which is the
 * same sharing the mark's halo/ink/hit paint already does. One ring recipe, one
 * declaration site.
 */
.tf-diagram__animate:focus,
.tf-diagram__toggle:focus {
  /* D-02: the 2px currentColor floor stays exactly as authored. Consuming
     --tf-ring-w/--tf-ring-color here would pin the reference coral onto chrome,
     violating the inherit-first isolation model (Phase 4, 25d7210). */
  /* tf-lint-allow: focus-floor */
  outline: 2px solid currentColor;
  outline-offset: var(--tf-ring-offset);
}

@supports selector(:focus-visible) {
  .tf-diagram__animate:focus,
  .tf-diagram__toggle:focus {
    outline: none;
  }
  .tf-diagram__animate:focus-visible,
  .tf-diagram__toggle:focus-visible {
    /* D-17: --tf-ring-color resolves through --tf-coral-deep to a fixed brand
       hex, so this is a deliberate palette consumption on chrome. currentColor
       cannot guarantee WCAG focus contrast against an unknown host ground, so
       the shared ring colour is the sole sanctioned non-inherited hue on widget
       chrome. Excused here rather than by carving --tf-ring-* out of the palette
       set, so a future palette token added beside it is still caught. */
    /* tf-lint-allow: focus-visible-ring */
    outline: var(--tf-ring-w) solid var(--tf-ring-color);
    outline-offset: var(--tf-ring-offset);
  }
}

/*
 * forced-colors fallback (mirrors the Steps/Tabs @media (forced-colors) block):
 * the focus ring falls back to Highlight so keyboard focus survives a
 * forced-colours theme.
 */
@media (forced-colors: active) {
  .tf-diagram__animate:focus,
  .tf-diagram__animate:focus-visible,
  .tf-diagram__toggle:focus,
  .tf-diagram__toggle:focus-visible {
    outline-color: Highlight;
  }
}

/*
 * Reduced-motion: HIDE the Animate control (D-04). Its only job is to trigger the
 * draw-in; under reduce the draw-in is instant, so a control that does nothing
 * perceptible is worse UX than no control. The diagram already shows the static
 * end-state, so hiding the control loses nothing. The description (aria-describedby
 * target) is NEVER hidden here.
 */
@media (prefers-reduced-motion: reduce) {
  .tf-diagram__animate {
    display: none;
  }
}

/*
 * 60-hotspot.css - the Hot Spot Diagram widget chrome (CATA-05, 12-UI-SPEC LOCKED).
 *
 * Authored per the LOCKED 12-UI-SPEC, reusing ~90% of the Phase 11 tf-diagram
 * visual system verbatim and following the shared inherit-first isolation model
 * (04-D-14 / RUNT-07, MEMORY: inherit-first-vs-pinned-palette):
 *   - box-sizing is the ONLY non-gate isolation (doubled root class + !important
 *     so a host rule cannot flip the widget's internal box model);
 *   - chrome is INHERIT-FIRST: `font: inherit` is load-bearing on the rehosted
 *     button.tf-hotspot__toggle (a native <button> does not inherit font), colour
 *     tracks `currentColor`, and NO palette token (--tf-accent / --tf-muted /
 *     --tf-white / the status trio) is pinned onto chrome, onto a badge, or onto
 *     any SVG shape. Only the shared focus ring (--tf-ring-color) may introduce a
 *     non-currentColor hue;
 *   - every BACKGROUND SVG element is styled by TYPE via currentColor (the 11/12
 *     "Per-shape SVG default treatment" table), stroke-width 2 in SVG user units
 *     tuned to the 0 0 100 100 viewBox the enhancer sets, scoped so the bare
 *     background shapes never inherit the runtime-owned badge paint.
 *
 * It DIVERGES from tf-diagram in exactly three ways (all 12-UI-SPEC):
 *   1. DEGRADATION GATE: unlike the diagram (whose body is injected, nothing to
 *      hide) the hot spot's region CONTENT is authored and present in the inert
 *      markup, so it IS concealed - but ONLY behind the compound
 *      html.tf-js-ready .tf-hotspot[data-tf-enhanced] gate (cloning
 *      10-reveal.css:29-37), NEVER a plain-class display:none. A runtime-absent or
 *      failed-to-enhance learner always reads the numbered label+content list.
 *   2. NO MOTION: the disclosure is INSTANT (a display flip only, no transition),
 *      so this sheet ships NO @media (prefers-reduced-motion) block - there is
 *      nothing to reduce (12-UI-SPEC A5).
 *   3. BADGE chrome: a runtime-owned outlined-disc + ink-number marker that carries
 *      the D-03 numbered affordance and the D-02 open/closed state entirely on
 *      currentColor + shape (never a pinned hue).
 */

/*
 * The only non-gate isolation: box-sizing. It does not inherit, so it is set
 * across the subtree explicitly, !important with the root class doubled to (0,2,0)
 * so a host rule cannot flip the widget's internal box model.
 */
.tf-hotspot.tf-hotspot,
.tf-hotspot.tf-hotspot * {
  box-sizing: border-box !important;
}

/*
 * The figure surface adopts the host's (inherit-first): no background and no
 * colour of our own. Generous outer padding stays - spacing is form, and form is
 * ours (matches tf-diagram + the reference @slice diagram outer padding).
 */
.tf-hotspot {
  padding: var(--tf-space-6);
}

/*
 * The caption: short title, semibold so it reads as a distinct affordance without
 * a pinned colour. Sits below the injected svg (the enhancer inserts the svg
 * BEFORE the figcaption), separated by a top margin. Label-sized on purpose
 * (matches .tf-diagram__caption).
 */
.tf-hotspot__caption {
  margin-top: var(--tf-space-4);
  font-size: var(--tf-fs-label);
  font-weight: var(--tf-fw-semibold);
  line-height: var(--tf-lh-tight);
}

/*
 * The region list. The <ol> gives the degraded list free 1..N numbering that
 * matches the runtime badges (D-03/D-04 correspondence). lg top margin separates
 * it from the svg above it.
 */
.tf-hotspot__regions {
  margin-top: var(--tf-space-5);
  padding-left: var(--tf-space-5);
}

.tf-hotspot__region + .tf-hotspot__region {
  margin-top: var(--tf-space-4);
}

/*
 * --- Per-shape BACKGROUND SVG treatment (11/12-UI-SPEC "Per-shape SVG default
 * treatment"). Scoped to the enhanced figure's injected svg and to DIRECT-child
 * shapes only, so the runtime-owned badge nodes (inside a <g class=tf-hotspot__
 * marker>) never inherit this bare-shape paint. Shapes carry GEOMETRY ONLY (the
 * emitter emits no fill/stroke); paint comes entirely from here via currentColor,
 * NO pinned palette. stroke-width 2 is SVG user units, tuned to the 0 0 100 100
 * canvas the enhancer sets. ---
 */

/* Boxes and outline containers read as outlines: no fill, currentColor stroke. */
.tf-hotspot[data-tf-enhanced] svg > rect,
.tf-hotspot[data-tf-enhanced] svg > ellipse {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

/* Connectors / leader lines: currentColor stroke. */
.tf-hotspot[data-tf-enhanced] svg > line {
  stroke: currentColor;
  stroke-width: 2;
}

/* Nodes and arrowheads read as solid marks: currentColor fill, no stroke. */
.tf-hotspot[data-tf-enhanced] svg > circle,
.tf-hotspot[data-tf-enhanced] svg > polygon {
  fill: currentColor;
  stroke: none;
}

/* Background labels: currentColor fill, host face (font-family inherit). */
.tf-hotspot[data-tf-enhanced] svg > text {
  fill: currentColor;
  font-family: inherit;
}

/*
 * --- The badge (new chrome, inherit-first - the D-03 numbered marker). A
 * runtime-owned <g class=tf-hotspot__marker> holding a <circle class=tf-hotspot__
 * badge> + a <text class=tf-hotspot__badge-num>. Selected by its OWN classes so it
 * does not inherit the bare-circle/bare-text background paint above. Default:
 * OUTLINED disc + ink number, so BOTH parts sit on currentColor and nothing needs
 * a host-surface colour pinned (12-UI-SPEC "Resolved (badge legibility)"). ---
 */
/*
 * The pointer cursor is QUALIFIED on the button role, not on the class. Since
 * v3.0 Phase 22 the same runtime-owned marker is painted inside a .tf-diagram by
 * the shared 00-core.js::buildOverlayBadge with its interactivity flag falsy, so
 * it carries no role and accepts no activation. An unqualified rule would have
 * that STATIC marker advertise a click it does not take. Behaviour-neutral for
 * the hot spot, whose every marker carries role="button".
 */
.tf-hotspot__marker[role="button"] {
  cursor: pointer;
}

.tf-hotspot__badge {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

.tf-hotspot__badge-num {
  fill: currentColor;
  font-family: inherit;
  font-weight: var(--tf-fw-semibold);
  /*
   * font-size in SVG USER UNITS, tuned to the 0 0 100 100 canvas - the same
   * convention `.tf-hotspot__badge`'s `stroke-width: 2` above sits under. Found
   * alongside UAT G-17-1's diagram defect and it is the SAME root cause: with no
   * font-size declared, the host's 16px is REINTERPRETED as 16 user units inside
   * the viewBox, so at the 500px cap the digit measured 106px tall inside a disc
   * only 100px across - the number was larger than the badge containing it.
   *
   * OVERLAY_BADGE_R is 10 (00-core.js), so the disc is 20 units across. 11
   * units gives a digit whose cap height sits comfortably inside that disc,
   * centred by the text-anchor/dominant-baseline pair below, and scales with the
   * badge at every width.
   */
  font-size: 11px;
  /* Centre the number in the disc (geometry-authored glyph). */
  text-anchor: middle;
  dominant-baseline: central;
}

/*
 * Open-state badge: tint the disc with a low-opacity currentColor fill (a non-hue
 * open/closed cue for sighted users; aria-expanded carries it for AT). A low
 * opacity - not a solid fill - is load-bearing ON THE HOST SURFACE: inherit-first
 * ships no surface colour to "knock out" the number against there, so a solid
 * currentColor disc would render the currentColor number invisible
 * (navy-on-navy). The tint keeps the number legible AND still reads as
 * filled/selected, all on currentColor with no palette token. Scoped to the
 * badge's own `aria-expanded="true"` (WR-01: the `<li>`-scoped
 * `.tf-hotspot__region--open` selector never matched because the badge lives in
 * the `<svg>`, not the `<li>`; the enhancer's toggle() sets aria-expanded on the
 * marker itself).
 *
 * THAT ARGUMENT IS NOW SCOPED RATHER THAN GENERAL, AND THE SCOPING IS THE POINT
 * OF THE BLOCK BELOW. "Inherit-first ships no knock-out surface" is true of the
 * HOST SURFACE, which is what this badge is drawn on. It is NOT true of an
 * approved illustration, which the host does not control and which supplies its
 * own ground - so over artwork the answer is the opposite one, a pinned light
 * value. The sentence above governs this badge only; the region marks below
 * carry the exception and its reasoning.
 */
.tf-hotspot__marker[aria-expanded="true"] .tf-hotspot__badge {
  fill: currentColor;
  fill-opacity: 0.18;
}

/*
 * --- IMAGE-MODE REGION MARKS (v3.1 Phase 22.1, D-18/D-19/D-21/D-22/D-23). ---
 *
 * A runtime-owned <g class=tf-hotspot__region-mark> holding THREE shapes built
 * from ONE scaled primitive: a halo stroke, an ink stroke and a transparent hit
 * surface. There is no badge and no digit on this path - the region's own outline
 * IS the mark, so nothing overlaps, nothing competes with the illustration's own
 * annotation, and the mark is exactly as large as the thing it means.
 *
 * These are SVG USER UNITS, tuned to the runtime-built viewBox, exactly as
 * `.tf-hotspot__badge`'s `stroke-width: 2` and `.tf-hotspot__badge-num`'s
 * `font-size: 11px` above are - see that block's worked comment for what happened
 * the last time a user-unit value was left to the host's CSS pixels. That is also
 * why NEITHER width below consumes `--tf-outline-w` or `--tf-hairline`: those two
 * are CSS px, and a CSS px inside a viewBox is a different length.
 */
/*
 * BOTH IMAGE-MODE WIDGETS SHARE THIS ONE DECLARATION SITE (v3.1 22.1-06). Since
 * 22.1-06 the DIAGRAM's image arm paints the same region trio from the same
 * 00-core.js builders, so its marks need the same alias. The selector is SHARED
 * rather than the value duplicated into 50-diagram.css: two declarations of one
 * pinned colour is two places for it to drift, and the exception D-22 records is
 * meant to be located in exactly one place. 50-diagram.css's inherit-first
 * paragraph is amended in place to name this site as the exception's home.
 */
.tf-hotspot[data-tf-image] .tf-hotspot__region-mark,
.tf-diagram[data-tf-image] .tf-hotspot__region-mark {
  /*
   * THE ONE PINNED VALUE IN THIS WIDGET, AND A SCOPED, REASONED EXCEPTION TO
   * INHERIT-FIRST (D-22). It is declared HERE, on the image-mode region selector,
   * rather than as a global token, so it cannot leak onto any chrome: it applies
   * ONLY to a mark drawn over an illustration and NEVER to anything on the host
   * surface.
   *
   * WHY THE RULE DOES NOT REACH HERE, written down rather than implied:
   * `currentColor` is a contract with the HOST SURFACE. An approved illustration
   * is NOT the host surface - it is content the host does not control - so a hue
   * collision with it cannot be fixed by any amount of opacity tuning. Measured
   * rather than assumed: currentColor resolved to rgb(43, 58, 85) on both engines
   * in Phase 22, which is invisible over dark navy artwork.
   *
   * The VALUE is not invented either. The house style guide already draws this
   * exact treatment for a hot-spot marker over artwork -
   * `runtime/reference/tf-interactions.css:519` gives its dot
   * `border: 3px solid var(--tf-white)`, an ink mark knocked out against a wider
   * light ring. So the light value the guide names is --tf-white, and this alias
   * consumes it rather than a literal. It deliberately does NOT consume `--tf-bg`,
   * whose meaning is "the host surface background" - the very thing a halo is
   * explicitly not drawn against.
   */
  /* tf-lint-allow: region-halo-over-artwork */
  --tf-region-halo: var(--tf-white);
  cursor: pointer;
}

/*
 * CLEAN AT REST (D-19). Nothing is painted until a region is activated, so a
 * learner sees the illustration exactly as the artist drew it. The at-rest
 * treatment is stroke-opacity rather than a display flip, so the hit surface
 * beneath stays present and tappable and the mark has somewhere to appear FROM.
 */
.tf-hotspot__region-halo,
.tf-hotspot__region-ink {
  fill: none;
  stroke-opacity: 0;
}

.tf-hotspot__region-halo {
  stroke: var(--tf-region-halo);
  /* Strictly WIDER than the ink, which is the whole of the dual-stroke idea: the
     halo shows beyond the ink on every ground. The 2:1 ratio below is what keeps
     that sentence true; shrinking one constant without the other would collapse
     the pair and redden the shipped dual-engine assertions that compare them.
   *
   * THINNED 3 -> 1 AND 1.5 -> 0.5 FOR UAT G-22.1-1 (v3.1 22.1-10). The operator
   * drew two regions over the real approved UAT0001-03 wrist illustration and
   * reported "the line around the item is too thick. It needs to be thinner."
   * THE ARITHMETIC, so the next reader does not have to rediscover why these
   * constants and not others: the widget's svg is capped at 500px wide (the
   * enhanced-layout rule below) and the viewBox is 100 units across, so ONE USER
   * UNIT IS 5 CSS PIXELS at the desktop cap. Below the 640px breakpoint the grid
   * collapses to one column, so at a 360px viewport the svg gets 296px after the
   * figure's 32px padding either side and a unit is about 2.96 CSS px.
   *   shipped 3 / 1.5 -> a ~15px halo band with a ~7.5px ink core at the cap
   *   now     1 / 0.5 -> a  ~5px halo band with a ~2.5px ink core at the cap,
   *                      leaving ~1.25px of halo showing on each side of the ink
   *
   * THE HALO IS DELIBERATELY NOT 2. runtime/tests/image.spec.js's falsification
   * twin injects the OLD descendant-form rule and asserts both strokes collapse
   * onto the bare author-shape width of 2; a halo authored at 2 would make that
   * "the value moved" assertion compare 2 against 2 and the falsification would
   * stop discriminating. 1 and 0.5 are chosen partly for that reason.
   *
   * THE STROKES STILL SCALE WITH CONTAINER WIDTH, DELIBERATELY. The operator
   * considered and REFUSED `vector-effect: non-scaling-stroke` for this fix: it
   * changes the rendering model and would force the shipped geometry tests to be
   * re-examined. This gap is the two constants and nothing else. */
  stroke-width: 1;
}

.tf-hotspot__region-ink {
  stroke: currentColor;
  /* Half the halo, per the ratio reasoned about above. */
  stroke-width: 0.5;
}

/*
 * `transparent`, NEVER `none` (D-23). An SVG shape with `fill: none` receives no
 * pointer events on its INTERIOR - measured on both engines, where
 * elementFromPoint over the middle of such a shape returns the <svg> rather than
 * the shape - so a fill:none region would be silently untappable everywhere
 * except along its own hairline. `transparent` paints nothing and receives
 * everything, which is exactly the split D-23 asks for: two concerns, two
 * elements, neither compromising the other.
 */
.tf-hotspot__region-hit {
  fill: transparent;
  stroke: none;
}

/* The OPEN region, and only the open one: the dual-stroke halo appears. Scoped to
   the mark's own aria-expanded, the same hook the badge open-state above uses and
   for the same reason - the mark lives in the <svg>, not in the <li>. */
.tf-hotspot__region-mark[aria-expanded="true"] .tf-hotspot__region-halo,
.tf-hotspot__region-mark[aria-expanded="true"] .tf-hotspot__region-ink {
  stroke-opacity: 1;
}

/*
 * --- THE SELECTED SIDE-LIST LABEL (v3.1 22.1-10, UAT G-22.1-2). ---
 *
 * WHAT THE GAP WAS. With a region open on a real approved illustration, the two
 * side-list labels read identically - nothing told the learner which one was
 * showing. The state was ALREADY IN THE DOM: 60-hotspot.js and 50-diagram.js
 * each write `aria-expanded` onto the side-list button as one half of a dual-sync
 * bound to a single contentId, so the mark and the button can never disagree.
 * Only the paint was missing. No JS changed for this, and the selector is the
 * ATTRIBUTE rather than a class precisely because that same attribute is what
 * carries the state to assistive technology.
 *
 * BOTH IMAGE-MODE ARMS, FROM THIS ONE DECLARATION SITE, exactly as the region
 * marks above and the D-20 instruction line below are declared once and shared.
 * The operator was asked and answered explicitly: both arms. Sharing the
 * SELECTOR rather than copying the rule into 50-diagram.css is what stops the
 * two lists drifting apart visually.
 *
 * RESERVED AT REST, REVEALED ON OPEN. The bar is declared `transparent` on every
 * image-mode toggle and only its COLOUR moves when a label is selected, so the
 * treatment cannot push the list sideways as a learner works down it. Adding the
 * border on open instead would grow the box by the outline weight at the exact
 * moment the learner's eye is on the picture.
 *
 * TWO CHANNELS, AND NEITHER IS A HUE. A learner sees a bar that was not there
 * and an underline that was not there - not a colour they have to be able to
 * name. The bar takes `currentColor`, so it tracks whatever ink the host
 * supplies; D-22's pinned-light-value exception is scoped to a mark drawn OVER an
 * illustration and does not reach a control sitting on the host surface, so
 * nothing here pins a palette token or a raw literal.
 *
 * `border-left`, NOT `border-inline-start`: the house sheets use physical
 * properties throughout and the frozen-WebView long tail predates the logical
 * ones. `border-left` is a POLICED property, so its width comes from
 * `--tf-outline-w` and never from a bare length.
 *
 * THE UNDERLINE IS THE LONGHAND, AND THAT IS NOT A STYLE PREFERENCE. The
 * `text-decoration` SHORTHAND is a colour-bearing shorthand in the D-09 rule, so
 * `text-decoration: underline` reports `underline` as an unknown colour
 * identifier against the positive allowlist in tests/test_style_tokens.py - and
 * the repair for that would be a new pragma or a widened allowlist, which this
 * gap does not justify. `text-decoration-line` carries no colour component, is
 * policed by nothing, and says exactly what is meant. The `-webkit-` alias is
 * carried for the same reason 20-flipcard.css carries its three: the unprefixed
 * longhand needs Safari 12.1 / Chromium 57, which is inside the frozen-device
 * long tail this runtime targets, and a WebView that understands neither still
 * gets the bar.
 */
.tf-hotspot[data-tf-image] .tf-hotspot__toggle,
.tf-diagram[data-tf-image] .tf-diagram__toggle {
  border-left: var(--tf-outline-w) solid transparent;
}

.tf-hotspot[data-tf-image] .tf-hotspot__toggle[aria-expanded="true"],
.tf-diagram[data-tf-image] .tf-diagram__toggle[aria-expanded="true"] {
  border-left-color: currentColor;
  -webkit-text-decoration-line: underline;
  text-decoration-line: underline;
}

/*
 * --- The disclosure gate (clone 10-reveal.css:29-37, INSTANT - no transition).
 * Region content is concealed ONLY behind the compound html.tf-js-ready
 * .tf-hotspot[data-tf-enhanced] gate, so a runtime-absent or failed-to-enhance
 * learner always reads it (RUNT-06 fail-safe, 12-UI-SPEC prohibition). Revealed
 * under the .tf-hotspot__region--open state. Display flip only, no motion. ---
 */
html.tf-js-ready .tf-hotspot[data-tf-enhanced] .tf-hotspot__content {
  /* !important so a host override cannot trap collapsed content or force it open
     against the toggle state (T-04-05 analogue). */
  display: none !important;
}

html.tf-js-ready
  .tf-hotspot[data-tf-enhanced]
  .tf-hotspot__region--open
  .tf-hotspot__content {
  display: block !important;
}

/*
 * --- THE IMAGE-MODE INSTRUCTION LINE (v3.1 Phase 22.1, D-20). ---
 *
 * One runtime-written <p> per image-mode widget, naming the direction that widget
 * leads with. Both widgets are styled from this ONE site, the same way the region
 * marks above are: the class token keeps the tf-hotspot__ prefix inside a
 * .tf-diagram (the accepted naming debt 00-core.js records), and sharing the
 * SELECTOR rather than copying the rule into 50-diagram.css is what stops the two
 * lines drifting apart visually.
 *
 * THESE ARE CSS PIXELS, NOT SVG USER UNITS, AND THE DISTINCTION IS SAID HERE
 * BECAUSE A READER OF THE ADJACENT RULES COULD EASILY GET IT WRONG. The region
 * strokes above, `.tf-hotspot__badge`'s `stroke-width: 2` and
 * `.tf-hotspot__badge-num`'s `font-size: 11px` are all authored in USER UNITS
 * inside a viewBox - see the worked comment at :148-160 for what happened the last
 * time that was forgotten. This line is ORDINARY HTML in the document flow, so
 * every length in it is a CSS pixel and every one of them comes from the house
 * scale.
 *
 * INHERIT-FIRST IN FULL, AND D-22'S EXCEPTION DOES NOT REACH HERE. That exception
 * exists because a mark drawn OVER an approved illustration has no contract with
 * the host surface. This line is chrome ON the host surface, exactly like the
 * caption beside it, so it takes the host's ink and the host's face and pins
 * nothing: no palette token, no literal colour, no raw dimension.
 */
.tf-hotspot[data-tf-image] .tf-hotspot__instruction,
.tf-diagram[data-tf-image] .tf-hotspot__instruction {
  margin: 0 0 var(--tf-space-3);
  color: inherit;
  font-size: var(--tf-fs-label);
}

/*
 * --- The rehosted region toggle. Built by the runtime from the authored
 * p.tf-hotspot__label; inherit-first: `font: inherit` is LOAD-BEARING (a native
 * <button> does not inherit font), colour tracks host ink, semibold is the resting
 * weight, NO pinned palette. The whole label row is the tap target, >= 44px tall
 * (WCAG 2.5.5). ---
 */
.tf-hotspot__toggle {
  display: block;
  width: 100%;
  min-height: var(--tf-hit);
  text-align: left;
  cursor: pointer;
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: var(--tf-fw-semibold);
  border: 0;
  margin: 0;
  padding: var(--tf-space-3) var(--tf-space-4);
}

/*
 * --- Focus ring (the badge <g> AND the toggle). The D-01 spike proved a :focus
 * outline computes on a focused SVG node on both engines. Plain :focus is the safe
 * currentColor floor; :focus-visible is a @supports-guarded enhancement to the
 * --tf-ring-* recipe (the sole sanctioned non-currentColor hue); forced-colors
 * falls back to Highlight. Mirrors 50-diagram.css:171-196. ---
 */
.tf-hotspot__marker:focus,
.tf-hotspot__region-mark:focus,
.tf-hotspot__toggle:focus {
  /* tf-lint-allow: focus-floor */
  outline: 2px solid currentColor;
  outline-offset: var(--tf-ring-offset);
}

@supports selector(:focus-visible) {
  .tf-hotspot__marker:focus,
  .tf-hotspot__region-mark:focus,
  .tf-hotspot__toggle:focus {
    outline: none;
  }
  .tf-hotspot__marker:focus-visible,
  .tf-hotspot__region-mark:focus-visible,
  .tf-hotspot__toggle:focus-visible {
    /* D-17: the "sole sanctioned non-currentColor hue" this file's header names
       at line 13. --tf-ring-color resolves through --tf-coral-deep to a fixed
       brand hex, and currentColor cannot guarantee WCAG focus contrast against
       an unknown host ground. Excused here rather than by carving --tf-ring-*
       out of the palette set, so a future palette token added beside it is
       still caught. */
    /* tf-lint-allow: focus-visible-ring */
    outline: var(--tf-ring-w) solid var(--tf-ring-color);
    outline-offset: var(--tf-ring-offset);
  }
}

@media (forced-colors: active) {
  .tf-hotspot__marker:focus,
  .tf-hotspot__marker:focus-visible,
  .tf-hotspot__region-mark:focus,
  .tf-hotspot__region-mark:focus-visible,
  .tf-hotspot__toggle:focus,
  .tf-hotspot__toggle:focus-visible {
    outline-color: Highlight;
  }
}

/*
 * --- Enhanced layout (12-UI-SPEC, operator-approved Phase 12 UAT). A two-column
 * grid: caption/title top-left, the injected svg on the left at a set width, and
 * the region labels+reveals in a right column that top-aligns with the image.
 * The image sits in its OWN grid row so the right column's height (reveals open
 * or closed) never moves the svg. Collapses to a single stacked column at
 * <=640px. Scoped to [data-tf-enhanced] so the no-JS degraded fallback (stacked
 * caption + numbered list) is unaffected.
 *
 * A THIRD ROW, `note`, WAS ADDED FOR v3.1 22.1's D-20 instruction line. It is an
 * EXPLICIT area rather than a left-to-auto-placement, and that is the point: an
 * unplaced grid item lands in the first free cell, which here is the `.` beside
 * the title, putting the instruction in the top-right corner instead of under the
 * heading. The row exists on the shapes-mode widget too and costs it nothing,
 * because a grid row with no item in it is zero-tall. ---
 */
.tf-hotspot[data-tf-enhanced] {
  display: grid;
  grid-template-columns: minmax(0, 500px) minmax(18rem, 1fr);
  grid-template-areas:
    "title ."
    "note ."
    "image text";
  grid-template-rows: auto auto auto;
  align-items: start;
  align-content: start;
  column-gap: var(--tf-space-6);
  row-gap: var(--tf-space-2);
}

.tf-hotspot[data-tf-enhanced] .tf-hotspot__caption {
  grid-area: title;
  margin: 0;
}

.tf-hotspot[data-tf-enhanced] .tf-hotspot__instruction {
  grid-area: note;
}

.tf-hotspot[data-tf-enhanced] svg {
  grid-area: image;
  align-self: start;
  display: block;
  width: 100%;
  max-width: 500px;
  max-height: 600px;
  margin: 0;
}

.tf-hotspot[data-tf-enhanced] .tf-hotspot__regions {
  grid-area: text;
  align-self: start;
  margin-top: 0;
}

@media (max-width: 640px) {
  .tf-hotspot[data-tf-enhanced] {
    grid-template-columns: 1fr;
    grid-template-areas:
      "title"
      "note"
      "image"
      "text";
  }
}
