/**
 * Express Entry Hub - Orb (ported from CT's v8 docked-panel orb)
 *
 * ★ SOURCE: confidence-talk/public/css/ct-orb.css (CT v2026.09.23.2)
 *   md5 a77195672b0c6da747639aeaacc6b3e2 · 2177 lines
 *   Provenance recorded in CLONE_LEDGER.md -> "Pass S-ORB -> S-ORB-0".
 *
 * Transformed by deterministic prefix rename only:
 *   ct-  -> eeh-   (65 tokens: classes, --custom-properties, keyframes)
 * No rule, value, comment or cascade position was changed. The markup IDs
 * (#eeh-avp-orb / .eeh-avp-ring / .eeh-avp-icon / .eeh-avp-label) are the
 * SAME four the orb already prints, so the sheet is drop-in.
 *
 * Styles for the 4-state voice orb. Replaces the old 11-state stylesheet
 * (`eeh-agent-voice-presence.css`, 558 lines) which had rules for
 * `reviving`, `deaf`, `rest`, `doze`, `sleep`, `holding`, `cancel` and
 * `attending` — states that no longer exist, because the loop that produced
 * them has been deleted.
 *
 * The orb reuses the SAME markup (#eeh-avp-orb / .eeh-avp-ring / .eeh-avp-icon /
 * .eeh-avp-label) so the existing PHP that prints it does not change. Only the
 * classes it drives change, from `eeh-avp-*` (11) to `eeh-orb-*` (4).
 *
 * Mobile-first: the orb is 56px (>= the 44px touch minimum in qa/config.json),
 * respects safe-area insets, and honours prefers-reduced-motion.
 *
 * @package ConfidenceTalk
 */

/* ── ★ THE ORB GEOMETRY CONTRACT (2026-09-22 docking rebuild) ────────────
 *
 * Every surface in this file is positioned from the orb, and the orb is
 * positioned from TWO constants that are defined here and referenced everywhere
 * else. They are CSS custom properties so the arithmetic can never drift:
 *
 *     --eeh-orb-size   the orb's diameter (64.4px desktop, 59.8px on small phones)
 *
 * ⚠★ S-ORB-TRIO (2026-10-11) — BOTH VALUES ARE 15% UP, AND THE ROUNDING IS CHOSEN
 *   SO THEY STAY PROPORTIONAL: 56 x 1.15 = 64.4 and 52 x 1.15 = 59.8. The owner
 *   asked for the orb and the "Vera is here" pill to be "15% bigger"; the pill is
 *   rounded to whole pixels (28 -> 32) because a 32.2px box is not a thing anyone
 *   can see, while the orb's value is left EXACT so the percentage stays honest and
 *   the derived geometry below (centre, panel clearance, ring inset) scales with it
 *   rather than snapping to a second rounded number.
 *     --eeh-orb-inset  its inset from the viewport edge (18px, 14px on small)
 *
 *     orb's outer (right) edge = --eeh-orb-inset
 *     orb centre (from right)  = --eeh-orb-inset + (--eeh-orb-size / 2)
 *                              = 18 + 28 = 46px   (desktop)
 *
 * ★ THE DOCK — WHY THE PANEL NOW SHARES THE ORB'S EDGES.
 *
 * The visitor's report (2026-09-22):
 *
 *     "the orb is at the bottom right and the caption window in dark is little
 *      above on the left … perhaps if it was inside the caption window to reduce
 *      the space covered by the whole chat interface … i see more empty gap on
 *      the right of the caption window, what if the orb … is on the extreme
 *      right, touching … the right circular borders"
 *
 * He was measuring something real. The previous design pinned the panel's right
 * edge to the orb's AXIS (`--eeh-orb-centre` = 46px) and left a deliberate 14px
 * vertical clearance above the orb — so the bar's right edge stopped 28px short
 * of the orb's right edge and 14px short of its top. That 28px sliver + the 14px
 * gap was real screen the interface did not use, and it read as two objects
 * rather than one.
 *
 * ── ★ DOCKED, NOT NESTED, AND THE DIFFERENCE MATTERS ──────────────────────
 * The orb was NOT moved into the panel's DOM. It is still a `position: fixed`
 * SIBLING appended to `<body>` by `_build()`, and the panel is still `z-index:
 * 9997` while the orb is `9998`. Only the panel's box moved. Three concrete
 * reasons nesting was rejected:
 *
 *   1. ★ THE MUTE'S REVEAL RULE IS A SIBLING SELECTOR. `_paint()` writes
 *      `data-eeh-orb-state` on `#eeh-avp-orb`, and the ONLY rule that makes the
 *      mute reachable while she speaks is
 *          #eeh-avp-orb[data-eeh-orb-state="speaking"] ~ .eeh-orb-panel .eeh-orb-mute
 *      Nesting the orb inside the panel would silently kill that `~`, and it is
 *      the one path that works on a TOUCH device (no hover). `V4`/`OB-29`/`OB-30`
 *      lock it, and it is an accessibility need, not decoration.
 *   2. The mobile stage (`_stage()`) drives BOTH elements with one class and
 *      expects the orb to be free to grow to a centred 112px hero. Re-parenting
 *      would make the staged orb a child of a full-screen sheet it must escape.
 *   3. The orb's own state machine, `_paint()`, `_collapse()` and the boot block
 *      all query `$("#eeh-avp-orb")` at body level.
 *
 * ── SO WHAT MAKES IT *LOOK* NESTED? ───────────────────────────────────────
 *   • the panel's `right` and `bottom` are now `--eeh-orb-inset` — the SAME two
 *     edges the orb uses, so they are flush;
 *   • the panel's bottom-right radius is fully round (999px) against the orb's
 *     own 50% circle, so the orb sits exactly ON the bar's right cap;
 *   • the panel carries `padding-bottom: calc(var(--eeh-orb-size) + 11px)` so the
 *     input row can never slide under the portrait.
 * The orb over the panel's rounded corner IS the "circular right end" that was
 * asked for, achieved with no DOM change at all.
 *
 * ⚠ The `--eeh-orb-*` values are declared on `:root` so BOTH `#eeh-avp-orb` and
 * `.eeh-orb-panel` can resolve them (they are siblings, not nested).
 * ⚠ EVERYTHING IN THIS BLOCK IS DESKTOP-ONLY (min-width: 481px). On a phone the
 * panel is a full-screen stage with `inset: 0` and `border-radius: 0`; the dock
 * must never leak into it. See the ≤480px block.
 */
:root {
  --eeh-orb-size: 77.3px;
  /* ★ S-TRIO-CLEAR (2026-10-11) — `18px` -> `20px`.
   *
   * At `18px` the orb cleared the viewport floor by `-1px`: her ring touched the
   * bottom edge on a 1885x1009 window. The FAB and the pill both sat at `+18px`.
   *
   * ⚠ THE OBVIOUS MOVE — raise `--eeh-orb-lift` — IS THE WRONG ONE. That token is
   *   SHARED by the panel and the pill, so raising it moves all three and re-breaks
   *   the centre alignment that took a probe to get right. The inset is the orb's
   *   own anchor, so moving it moves only her.
   *
   * ⚠ AND IT MOVES HER CENTRE TOO, so the offset is `+2px` and not `+1px`: the
   *   centre now sits at `974` while the FAB is at `972`. Two pixels is inside the
   *   measurement's own rounding and is the smallest value that clears the floor —
   *   a third pixel would buy nothing and cost another tick of misalignment.
   *
   * ★★ S-ORB-PAIR-EDGE (2026-10-11) — `20px` -> `22px`, SO THE MARGIN SHE RESTS AT
   *    IS THE MARGIN SHE STOPS AT.
   *
   * ⚠ THE OWNER PICKED THIS EXPLICITLY, and the reason is the point: at `20px` the
   *   group rested at one margin and was clamped at another. The clamp now stops the
   *   pair at `22px` (matching the "Hear this page" button), so leaving the REST at
   *   20px would have made the group visibly jump the moment a drag began.
   *
   * ⚠ IT MOVES THE WHOLE GROUP 2px LEFT AT REST, WHICH IS THE APPROVED COST. The pill
   *   and the orb share `--eeh-orb-x`, so this shifts both. The owner approved it with
   *   that stated.
   *
   * ⚠ AND IT MOVES HER CENTRE 2px AGAIN (972 -> 974 against a FAB at 972). The
   *   alignment probe measures within its own tolerance and this is verified by
   *   `probe-orb-field-rendered` and `probe-orb-aov-align` rather than assumed.
   */
  --eeh-orb-inset: 22px;
  /* The orb's horizontal centre, measured from the viewport's right edge. */
  --eeh-orb-centre: calc(var(--eeh-orb-inset) + (var(--eeh-orb-size) / 2));
  /* ★ THE BREATHING ROOM BESIDE HER (v7). The gap between the orb and the
     "Type or talk" pill. It is a CONSTANT rather than a per-element margin so
     that anything later placed in the orb's row spaces itself the same way —
     the same reasoning as `--eeh-orb-inset`. */
  --eeh-orb-gap: 10px;
  /* The one gold. Every accent in this file reads from here. */
  /* ★★ THE ORB THEME CONTRACT (v8) — THE DEFAULTS, AND THEY MATTER.
   *
   * These are the SHIPPED VALUES. PHP inlines the admin's chosen theme on `:root`
   * (see `CT_Agent_Voice_Presence::orb_theme_css()`), and because that block is
   * emitted in the same `:root` scope but LATER in the cascade, the admin's values
   * win. ⚠ These fallbacks exist for the case where the theme block is absent — a
   * cached page, a theme freshly added, a PHP error — and they are also what
   * `qa/orb_theme_test.py` treats as "the default theme's expected values".
   *
   * ⚠ DECLARE EVERY NEW PROPERTY HERE OR THE ORB BREAKS SILENTLY. A missing
   * `--eeh-panel-metal-a` would make `linear-gradient()` invalid, which drops the
   * WHOLE background-image declaration — the panel would render flat with no
   * gradient and no error. That is why the gradient is written with `var(…, <literal>)`
   * fallbacks at the point of use as well as here: two independent layers, because
   * this failure has no visible error message. */
  --eeh-orb-gold: #cd853f;
  --eeh-orb-gold-lit: #e09a52;
  --eeh-orb-ink: rgba(18, 18, 20, 0.82);
  --eeh-panel-metal-a: rgba(58, 58, 66, 0.92);
  --eeh-panel-metal-b: rgba(30, 30, 34, 0.90);
  --eeh-panel-metal-c: rgba(10, 10, 12, 0.94);
  --eeh-panel-edge: rgba(255, 255, 255, 0.12);
  --eeh-panel-sheen: rgba(255, 255, 255, 0.05);
  --eeh-orb-text: #ffffff;
  --eeh-orb-dim: rgba(255, 255, 255, 0.62);
}

/* ── the orb ─────────────────────────────────────────────────────────── */

#eeh-avp-orb {
  position: fixed;
  /* DRAG (2026-10-01): the dock offset is SUBTRACTED here.
     `_setDock()` publishes `--eeh-orb-x` / `--eeh-orb-y` on `:root`; dragging
     LEFT makes `--eeh-orb-x` NEGATIVE, which GROWS `right` and moves her left.
     Zero when never dragged, so the default layout is unchanged.
     NOTE the anchor form is EEH's own (`max(inset, env(safe-area-inset-*))` +
     `bottom`), not CT's - the offset is added to what is here, not copied. */
  right: calc(max(var(--eeh-orb-inset), env(safe-area-inset-right, 0px)) - var(--eeh-orb-x, 0px));
  /* ══ ★★ S-ORB-TRIO-2 (2026-10-11) — SHE RISES TO MEET THE PILL'S LINE ════════
   *
   * ── THE OWNER'S REQUEST, SECOND HALF ──────────────────────────────────────
   *   "keep the present size of the orb, but move it up, so it is in line to the
   *    hear this page button."
   *
   * ── ★ THE ARITHMETIC, DERIVED RATHER THAN GUESSED ─────────────────────────
   * Two bottom-anchored elements share a centreline only when their CENTRES agree:
   *
   *     centre = viewportBottom - inset - (height / 2)
   *
   * The pill's size is back to 56px and its inset never moved, so its centre is
   * fixed at `V - 18 - 28`. The orb is now the TALLER one, so at an equal `bottom`
   * her centre lands (H_orb - H_pill) / 2 = 4.2px ABOVE the pill's — and the fix is
   * to move her centre DOWN by that much, which means REDUCING her `bottom` by 4.2.
   *
   * ⚠ THE SIGN IS `-`, AND THE FIRST ATTEMPT HAD IT `+` AND WAS MEASURABLY WRONG.
   *   A bottom-anchored box sits higher up the screen as its `bottom` grows, so a
   *   taller box needs a SMALLER `bottom` to bring its centre down onto a shorter
   *   one's. Adding the term pushed her centre 4.2px the wrong way and the probe
   *   reported an 8.4px spread — the arithmetic below is written out so the next
   *   reader does not have to re-derive the direction from first principles.
   *
   *     centre = viewportBottom - bottom - height / 2
   *     want orbCentre == pillCentre
   *       V - b - Ho/2 = V - 18 - Hp/2
   *       b = 18 - (Ho - Hp)/2            <- SUBTRACT
   *
   * ⚠ WHY IT IS AN EXPRESSION AND NOT `- 4.2px`. A literal is correct for exactly
   *   one pair of heights and silently wrong the moment either changes — which is
   *   the failure this pass exists to undo: in S-ORB-TRIO the PILL was grown to
   *   match the orb, so the control the owner did not want touched is the one that
   *   moved. Written as a relationship, the next size change keeps them aligned.
   *
   * ⚠ `--eeh-aov-pill-h` CARRIES ITS OWN FALLBACK (56px) because the pill's sheet is
   *   enqueued by a DIFFERENT class on different conditions. On a page where the
   *   emitter did not run this still evaluates to the correct 4.2px rather than 0 —
   *   the orb would otherwise drop out of line on exactly the pages that have no
   *   theme block (cached, DB reset, guide switched off).
   */
  bottom: calc(max(var(--eeh-orb-inset), env(safe-area-inset-bottom, 0px))
               - (var(--eeh-orb-size) - var(--eeh-aov-pill-h, 56px)) / 2
               - var(--eeh-orb-y, 0px)
               /* ★ S-TRIO-LIFT — the shared group lift. ADDED, never replacing
                  `--eeh-orb-y`: that token is written by the drag on every
                  _setDock(), so a value placed there would be wiped. */
               + var(--eeh-orb-lift, 0px));
  width: var(--eeh-orb-size);
  height: var(--eeh-orb-size);
  border-radius: 50%;
  cursor: pointer;
  z-index: 9998;
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: manipulation;          /* no 300ms tap delay on mobile */
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.18s ease, box-shadow 0.18s ease, width 0.28s ease,
              height 0.28s ease, bottom 0.28s ease, right 0.28s ease;
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.35);
  /* ★ LESS BLACK AROUND HER (2026-09-22, v4).
   *
   * The visitor: "make the hostess image little clearer, as looks like shaded".
   *
   * This disc sits directly behind the portrait and reads as a vignette around a
   * circular photo. At `rgba(18,18,20, 0.72)` plus `blur(6px)` it was doing two
   * darkening things at once: tinting the area around her almost opaque black, and
   * smearing the page behind it so the edge of the circle had no contrast to play
   * against.
   *
   * ⚠ IT CANNOT SIMPLY BE REMOVED. It is the guarantee that her face is legible
   * over ANY page — a light hero image, a white blog post, a busy gallery. That is
   * the whole reason a fixed floating orb has a backing at all. So it is LIGHTENED
   * rather than deleted: enough show-through that she reads as sitting on the page,
   * still enough darkness that a white background cannot wash her out.
   *
   * The blur drops further than the tint because blur is what made it look like
   * haze rather than depth. */
  background: rgba(18, 18, 20, 0.55);
  backdrop-filter: blur(3px);
}

/* ══ ★★ THE DRAG SUPPRESSES THE DOCK TRANSITION (2026-10-07, S-ORB-DRAG) ═══
 *
 * ── THE BUG THIS CLOSES ───────────────────────────────────────────────────
 *     "the dragging is not good, like CT, feel like the caption and the orb
 *      not moving together"
 *
 * ── ROOT CAUSE (measured, not guessed) ────────────────────────────────────
 * The orb carries `transition: … bottom 0.28s ease, right 0.28s ease` (above)
 * for the dock animation, and the PANEL shares the same `--eeh-orb-x/y` offset
 * (see `.eeh-orb-panel`). Left on during a drag, EVERY `pointermove` starts a
 * fresh 0.28s ease toward the new position — so the ORB visibly LAGS the
 * pointer while the PANEL, whose own transition block does NOT list `right`/
 * `bottom`, snaps instantly. The two surfaces therefore arrive at different
 * times and read as "the caption and the orb are not together".
 *
 * ── THE FIX IS CT'S, PORTED ───────────────────────────────────────────────
 * ConfidenceTalk solves it exactly here (`ct-orb.css:239-242`, CT v2026.10.06.5):
 * the JS sets `data-ct-orb-dragging="1"` and CSS kills the transition for the
 * whole gesture, on BOTH surfaces. EEH's JS sets the SAME attribute
 * (`eeh-orb-loop.js:2431` → `data-eeh-orb-dragging="1"`) but the CSS half of the
 * port was dropped — so the attribute was set and consumed by nobody.
 *
 * ⚠ BOTH SELECTORS ARE REQUIRED. Suppressing only the orb would leave the panel
 * trailing by a quarter second; suppressing only the panel would leave the orb
 * lagging. They share one offset, so they must share one transition state.
 *
 * ⚠ NOT `!important`, matching CT. The reduced-motion block already sets
 * `transition: none !important` on the orb, and this attribute selector (1,1,0)
 * is strong enough to beat the base rule (1,0,0) without escalating.
 *
 * ⚠ `data-eeh-orb-dragged` IS STICKY (set once, never cleared) — it advertises
 * that she is movable, so the cursor stays `grab` after she has been moved. */
#eeh-avp-orb[data-eeh-orb-dragging="1"],
#eeh-avp-orb[data-eeh-orb-dragging="1"] ~ .eeh-orb-panel {
  transition: none;
}
#eeh-avp-orb[data-eeh-orb-dragging="1"] {
  cursor: grabbing;
  /* Lifted: the shadow deepens so she reads as picked off the page. Purely a cue —
     no geometry depends on it, so it cannot break the dock contract. */
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
}
#eeh-avp-orb[data-eeh-orb-dragged="1"] {
  cursor: grab;
}

#eeh-avp-orb:hover,
#eeh-avp-orb:focus-visible {
  transform: scale(1.06);
  outline: none;
}

/* The portrait (hostess avatar), set via --eeh-avp-portrait by PHP. */
#eeh-avp-orb .eeh-avp-icon {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: var(--eeh-avp-portrait, none) center / cover no-repeat;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 26px;
  line-height: 1;
}

/* ── the ring (the ONE animation surface) ────────────────────────────── */

#eeh-avp-orb .eeh-avp-ring {
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  border: 2px solid rgba(205, 133, 63, 0.55);
  pointer-events: none;
  transition: border-color 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
  opacity: 0.75;
}

/* ── ★ THE SLEEP AFFORDANCE: `−` IS A SHARED CONTROL (2026-09-22, v6) ─────────
 *
 * ── WHAT WAS WRONG: DESKTOP HAD NO WAY TO PUT HER BACK TO SLEEP ───────────
 * The `−` button already existed — inside `.eeh-orb-mobile-bar`, whose only rules
 * lived INSIDE the `@media (max-width: 480px)` block. So on desktop there was no
 * minimise affordance at all, and once the bar was revealed it stayed revealed
 * for the rest of the session. The visitor asked for the opposite:
 *
 *     "can we have a process, that orb remains in circle, and with short message
 *      like talk to me, in other wards, it is in minimized form, and quite, and
 *      asking user to wake her up, and once awaken the caption windows shows up
 *      and so on"
 *
 * ★ AND THE MEANING WAS ALREADY DOCUMENTED. `.clinerules` locks the mobile
 * behaviour: "`_collapse()` is NOT `_finish()`. Minimising hides the panel and
 * nothing else: no teardown, mic stays open, session continues." So `−` = SLEEP
 * was already the intended contract — it simply never reached desktop. Promoting
 * the control is therefore not a new concept; it is finishing one.
 *
 * ── WHY IT IS INVISIBLE AT REST, LIKE THE MUTE ────────────────────────────
 * The resting stripe's balance is the thing the visitor has been iterating on for
 * several rounds, so the sleep control must not disturb it. It ships at
 * `opacity: 0` (STILL IN THE LAYOUT, so the bar's geometry never shifts) and
 * appears on hover, on `:focus-visible`, and once engaged — the exact same
 * contextual pattern as `.eeh-orb-mute`, which is also the only pattern that works
 * on a TOUCH device, where there is no hover at all.
 *
 * ⚠ IT IS NOT `display: none`. A `display: none` control cannot be focused or
 * announced, so keyboard and screen-reader visitors could never sleep her.
 * `opacity: 0` keeps it reachable by Tab while keeping it invisible to the eye —
 * the same trade `.eeh-orb-mute` makes and which `V4` locks for the mute.
 */
@media (min-width: 481px) {
  /* ⚠⚠ THE WRAPPER: `height: 0` AND NOTHING ELSE (v6b — SIMPLIFIED).
   *
   * The v6 version of this rule carried a long history: the button used to live
   * inside this wrapper, absolutely positioned, and every attempt to place it needed
   * the wrapper to be a box. Three separate rounds of geometry work went into making
   * that work (out-of-flow vs. containing block, specificity, z-order over the orb's
   * expanded ring).
   *
   * ★ ALL OF IT IS NOW MOOT: THE BUTTON MOVED INTO `.eeh-orb-head`. The only thing
   * left to solve here was that this wrapper had `display: none` on desktop while
   * still being laid out by the phone rules, so its height is simply zeroed. There
   * is no positioning left to get wrong. The grip is hidden because a "pull this
   * sheet away" affordance means nothing on a bar.
   *
   * ⚠ DO NOT MOVE THE FOLD CONTROL BACK IN HERE. It is in the head now, and the
   * reason is not cosmetic: a control in the wrapper has to fight the input row, the
   * orb's docked position and the orb's z-index (9998 vs the panel's 9997), whereas
   * in the head it shares a row with nothing but text. That is a structural win, not
   * a tidy-up. */
  .eeh-orb-panel .eeh-orb-mobile-bar {
    display: block;
    height: 0;
    min-height: 0;
    padding: 0;
    overflow: visible;
  }
  .eeh-orb-panel .eeh-orb-grip { display: none; }

  /* ── ★ THE FOLD CONTROL — REMOVED (S-ORB-NO-MINUS, 2026-10-10) ─────────────
   *
   * A `min-width: 481px` block lived here setting `.eeh-orb-min { margin: 0 -5px
   * 0 2px }` — "how much room the row can spare" for the minus.
   *
   * ⚠ ITS OWN COMMENT SAID "DO NOT MOVE THE FOLD CONTROL BACK IN HERE", AND THE
   * CONTROL IS NOW GONE ENTIRELY. The visitor reversed the v7 request:
   *
   *     "the minus - sign minimize the caption windows, to the orb, how about
   *      removing this, keep the speaker icon only, that gives more lenght to
   *      the text box also, will look balanced, and the reducing or shutting her
   *      off funciton can be achived, by clicking the orb circle"
   *
   * ★ SO THE MARGIN GOES WITH IT. It was tuning space for a control that no
   * longer exists; leaving it would be a rule matching nothing. The row is now
   * `[ 🔊 speaker ] [ ————— field ————— ]` and the field takes the freed width
   * automatically (`.eeh-orb-input` is already `flex: 1 1 auto; min-width: 0`).
   *
   * ⚠ THE ACTION MOVED TO THE ORB, IT DID NOT DISAPPEAR — `_bind()` toggles
   * `_collapse()` on the orb's click/keydown. See the base removal note and the
   * gate's S-ORB-NO-MINUS assertions. */
}

/* ── ★ THE "TYPE OR TALK" PEEK — THE INVITATION BESIDE THE ORB (v7) ──────
 *
 * ── WHAT IT IS ───────────────────────────────────────────────────────────
 * One small pill to the LEFT of the docked orb, carrying the same two words as
 * the field's placeholder. It answers the only question a cold visitor has about a
 * floating circle: "what is that, and what do I do with it?"
 *
 * ── ★ THE THREE PLACEMENTS, AND WHY ONLY THE THIRD IS ALLOWED ────────────
 * This element has been built and deleted TWICE, and both failures were the SAME
 * failure — the hint sat in the CAPTION'S BAND:
 *   • v1 (2026-09-15): ABOVE the orb (`bottom: 100% + 10px` → `84..106px`). The
 *     caption band was `86..174px`. The same 20px. "Partially on top of orb,
 *     visibility is bad."
 *   • v6 (2026-09-22): BELOW the orb. The collision was genuinely fixed and the
 *     visitor still rejected it: "remove the talk to me text the bottom of the orb".
 * ★ v7 IS TO THE LEFT, IN THE ORB'S OWN ROW, and that is a different thing rather
 * than a third guess. The caption already RESERVES the orb's full column through
 * `--eeh-orb-clear`, so a pill inside that reserved strip cannot reach her words —
 * the overlap is prevented by the caption's own existing rule, not by a new one.
 *
 * ── ★ POSITIONED FROM THE SAME TWO CONSTANTS AS THE ORB ──────────────────
 *   orb left edge (from viewport right) = --eeh-orb-inset + --eeh-orb-size
 *   this pill's right edge              = that + --eeh-orb-gap
 * ⚠ NO NEW MAGIC NUMBER. `--eeh-orb-size` and `--eeh-orb-inset` are the two
 * properties the entire file is positioned from (see the geometry contract at the
 * top), so the pill is flush with her BY CONSTRUCTION and cannot drift out of line
 * when the orb's size changes across breakpoints.
 *
 * ── ≜ VERTICALLY CENTRED ON HER, NOT ON THE VIEWPORT ─────────────────────
 * `bottom` is anchored to the orb's own CENTRE LINE (inset + size/2) and the pill
 * is lifted by exactly half its OWN height via `transform: translateY(50%)`, so the
 * centring is EXACT for any pill height rather than depending on a literal that had
 * to equal half a hardcoded `height`. A pill aligned to the viewport's bottom
 * margin would sit visibly low against a 56px orb.
 *
 * ⚠ `pointer-events: none`. It is an invitation, not a control — the thing to tap
 * is the ORB, and a pill that swallowed taps would steal them from the one target
 * that works. It is `aria-hidden` in the markup for the same reason.
 *
 * ⚠ DESKTOP ONLY (min-width: 481px). On a phone the orb becomes a centred 112px
 * hero inside a full-screen stage with its own typography, so a floating pill
 * beside it would land in the middle of the layout with nothing to align to.
 */
.eeh-orb-peek {
  position: fixed;
  right: calc(var(--eeh-orb-inset) + var(--eeh-orb-size) + var(--eeh-orb-gap) - var(--eeh-orb-x, 0px));
  /* ══ ★ S-ORB-TRIO-2 — THE PILL FOLLOWS HER, OR IT LEAVES THE LINE ═══════════
   *
   * The pill is anchored to the orb's CENTRE (`inset + size/2`), not to its own
   * height, so the moment the orb rises the pill must rise with it or the two come
   * apart — the documented S-ORB-PEEK-ALIGN defect, in the opposite direction.
   *
   * ⚠ THE `+ rise` TERM IS THE SAME EXPRESSION THE ORB USES, and it is repeated
   *   rather than shared because this is a SIBLING rule, not a descendant: the orb's
   *   computed `bottom` cannot be read from here. The duplication is safe precisely
   *   because BOTH sides derive from the same two tokens, so they cannot disagree —
   *   which is the property the last three passes kept paying for. A hardcoded 4.2px
   *   here would be a second answer to the same question and would drift.
   */
  bottom: calc(var(--eeh-orb-inset)
               - (var(--eeh-orb-size) - var(--eeh-aov-pill-h, 56px)) / 2
               + (var(--eeh-orb-size) / 2)
               - var(--eeh-orb-y, 0px)
               /* ★ S-TRIO-LIFT — the shared group lift. ADDED, never replacing
                  `--eeh-orb-y`: that token is written by the drag on every
                  _setDock(), so a value placed there would be wiped. */
               + var(--eeh-orb-lift, 0px));
  transform: translateY(50%);
  z-index: 9997;                    /* under the orb (9998), over the page */
  display: flex;
  align-items: center;
  /* ══ ★★ S-ORB-TRIO (2026-10-11) — 15% BIGGER PILL, AND A CENTRELINE THE TOP
   *    EDGE ALSO HONOURS ═════════════════════════════════════════════════════
   *
   * ── THE OWNER'S REPORT ────────────────────────────────────────────────────
   *   "now make the caption button, vera is here, and the orb size 15% bigger,
   *    and also make it in the same line to Hear this page, as the moment it is
   *    little lower"
   *
   * ── ★ MEASURED FIRST, AND THE MEASUREMENT CORRECTS THE REPORT IN A USEFUL WAY ─
   * BEFORE this pass the three centrelines were ALREADY identical:
   *
   *     orb  h=56  centreY=854
   *     peek h=28  centreY=854      <- same centreline
   *     fab  h=56  centreY=854
   *
   * So "it is little lower" was NOT a centring bug. It is the thing a 28px pill
   * beside a 56px orb LOOKS like: with equal centres, the pill's TOP edge sits
   * 14px below the orb's, and the eye reads that gap as "the small one is lower".
   * Making the pill 15% bigger (28 -> 32) closes half of it; the remainder is the
   * irreducible difference between a circle and a capsule.
   *
   * ⚠ SO THE HONEST FIX IS `transform: translateY(50%)` PLUS A REAL HEIGHT — NOT A
   *   NUDGE. The old `translateY(50%)` centres the pill correctly at 28px AND at
   *   32px, because it is expressed against the pill's OWN height. Any attempt to
   *   "lift" it with a fixed pixel offset would be correct at exactly one size and
   *   would break the moment the height changed again.
   *
   * ── ★ 15% BIGGER: 28px -> 32px ─────────────────────────────────────────────
   * `Math.round(28 * 1.15) = 32`. The font moves with it (12.5px -> 14.4px) so the
   * label keeps its optical proportion rather than floating in a taller pill;
   * horizontal padding stays proportionate too (12px -> 14px).
   *
   * ⚠ `.eeh-orb-peek` IS STILL `pointer-events: none`. Enlarging a decorative pill
   *   must not turn it into a click target that steals taps aimed at the orb — the
   *   orb is the target, always. That line is deliberately untouched.
   */
  height: 38.4px;
  padding: 0 14px;
  border-radius: 999px;
  /* ★ S-ORB-TRIO-3 — the pill grew 32 -> 38.4px to MATCH the FAB's height, and the
     font moved with it (14.4 -> 14.5px) so both controls read as one pair. */
  /* ★ FONT SCALES WITH THE BOX, so the pill grows as ONE object. Leaving the text
     at 12.5px would make the taller pill read as padded rather than larger. */
  font-size: 14.5px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: rgba(255, 255, 255, 0.86);
  background: var(--eeh-orb-ink);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  pointer-events: none;             /* the ORB is the target, never this */
  user-select: none;
  /* ══ ★★ S-TRIO-SURFACE (2026-10-11) — THE PILL WEARS THE SAME SURFACE AS THE BUTTON
   * ═══════════════════════════════════════════════════════════════════════════════
   *
   * ── WHY ───────────────────────────────────────────────────────────────────────
   * The owner, after the sizes and the centreline were already matched to 0px:
   *   "so the two looks in balance to each other"
   * A preview harness measured the two controls side by side and showed that the
   * GEOMETRY was identical and the SURFACE was not:
   *
   *   property       PILL (this)            FAB (.eeh-aov-fab)
   *   height         38.4px                38.4px       MATCHED
   *   font-size      14.5px                14.5px       MATCHED
   *   font-weight    500                   600          <- this pass
   *   border         none                  1px hairline <- this pass
   *   box-shadow     0 2px 10px .28        0 6px 18px .35  <- this pass
   *   background     var(--eeh-orb-ink)    var(--eeh-aov-ink)  same palette 'ink'
   *
   * So the two boxes were identical and they still read as a mismatched pair: a
   * light-weight label sitting beside a heavier button. The imbalance was never
   * vertical and no amount of lifting could have fixed it — which is why the
   * measured answer ("already aligned") and the owner's eye disagreed.
   *
   * ── DIRECTION: THE PILL COMES UP TO THE BUTTON, NOT THE REVERSE ───────────────
   * • The pill is the OUTLIER (500 against the FAB's 600), so changing it is the
   *   smaller edit and it is the element that was never conforming.
   * • The FAB is INTERACTIVE. Stripping its border would make a tappable control
   *   look like a passive label — a usability loss, not a tidy-up.
   * • The ♪ ICON IS DELIBERATELY NOT COPIED. It carries the playing state and the
   *   equaliser animation, and it is only on the FAB because that is the thing you
   *   press. This pill is `aria-hidden` decoration shown while Vera is idle and
   *   `pointer-events: none`; giving it the glyph would advertise a control that
   *   cannot be clicked.
   *
   * ── ⚠ `box-sizing: border-box` IS THE LOAD-BEARING LINE ───────────────────────
   * This element has a PINNED `height: 38.4px`. Without `border-box`, a 1px border
   * is added OUTSIDE that height, making the pill 40.4px — silently destroying the
   * exact match the previous pass established and that this pass exists to finish.
   * The FAB already forces `border-box` for the same reason; the pill had no border
   * so it never needed it. It needs it now. DO NOT REMOVE IT.
   */
  font-weight: 600;
  border: 1px solid var(--eeh-orb-edge, rgba(205, 133, 63, 0.50));
  box-sizing: border-box;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}

/* ⚠ A NARROW DESKTOP IS NOT A PHONE. Below ~560px the orb and a ~104px pill
   together exceed the viewport, so the pill is dropped rather than allowed to
   collide with the orb or run off-screen. The orb's own aria-label and the open
   bar's placeholder still carry the instruction there. */
@media (max-width: 560px) {
  .eeh-orb-peek { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .eeh-orb-peek { transition: none; }
}

/* ── the status pill: REMOVED (2026-09-15) — AND STILL REMOVED (v6b/v7) ───
 *
 * The hover pill (`#eeh-avp-orb .eeh-avp-label`) has been DELETED, not restyled.
 *
 * WHY IT WAS ORIGINALLY DELETED: it sat ABOVE the orb
 * (`bottom: calc(100% + 10px)` → 84..106px) while the caption panel sat at
 * 86..174px — the SAME 20px band. Two text boxes drew over each other in different
 * colours, which was the reported "partially on top of orb, visibility is bad".
 * A hover-only pill is also invisible on touch, and it duplicated the caption,
 * which is always visible and does the job properly.
 *
 * ── ★ IT WAS BRIEFLY REVIVED (v6) AND THE VISITOR REJECTED IT (v6b) ────────
 * v6 brought it back to carry "Talk to me" BELOW a sleeping orb, which genuinely
 * fixed the collision above. The visitor's verdict was immediate and final:
 *
 *     "remove the talk to me text the bottom of the orb"
 *
 * ★ SO IT IS HIDDEN AGAIN, AND THE LESSON IS NOT "PUT IT IN A BETTER PLACE". The
 * v6 placement was measurably correct — `V19` proved the label sat clear of the
 * caption band — and it still had to go. A second text surface floating beside her
 * competes with her portrait for attention and says something she can say herself.
 * ⚠ "The geometry was right" is NOT the same as "the element belongs". The 2026-09-15
 * removal and the 2026-09-22 removal agree, which is the strongest signal available
 * that this element should stay gone.
 *
 * ── WHAT CARRIES THE SLEEPING ORB NOW ─────────────────────────────────────
 * MOTION AND FORM ALONE: the ring breathes (`eeh-orb-sleep`) and she is a bare
 * circle with no bar. `prefers-reduced-motion` suppresses the pulse, which is
 * acceptable BECAUSE NOTHING IS LOST BY IT — waking her is the same gesture it has
 * always been (tap the orb), and no text invitation was ever the only way in.
 *
 * ⚠ DO NOT RE-USE THIS ELEMENT FOR INVITATIONS, HINTS OR HOVER COPY. It is kept in
 * the markup for parity with the PHP that prints it; the PHP/JS contract is
 * unchanged. `data-eeh-orb-asleep` still marks a sleeping orb — it is simply no
 * longer wired to this node.
 */
#eeh-avp-orb .eeh-avp-label {
  display: none !important;
}

/* ── she is asleep: the ring breathes ────────────────────────────────────── */
#eeh-avp-orb[data-eeh-orb-asleep="1"] .eeh-avp-ring {
  animation: eeh-orb-sleep 2.6s ease-in-out infinite;
}

/* ★ A BREATH, NOT A BEAT. 1.07 here against the speaking ripple's 1.18: the
 * sleeping orb must be visibly the QUIETEST state she has, or "asleep" reads as
 * "busy". `prefers-reduced-motion` removes this globally (see that block) — and
 * that is a deliberate, accepted trade now that the pulse is the ONLY signal: a
 * visitor who suppresses motion still sees a bare 56px circle with no bar, which
 * is the quiet state they asked for rather than a broken one. */
@keyframes eeh-orb-sleep {
  0%, 100% { transform: scale(1);    opacity: 0.55; }
  50%      { transform: scale(1.07); opacity: 0.30; }
}

/* ── ⚠ THE `:has()` GUARD WAS REMOVED (2026-09-22, v6) — AND IT IS WORTH RECORDING WHY.
 *
 * The first version added a belt-and-braces rule so an open panel could never show
 * the invitation:
 *
 *     #eeh-avp-orb[data-eeh-orb-asleep="1"]:has(~ .eeh-orb-panel:not([hidden])) …
 *
 * IT MEASURED AS A BUG. The suite caught the label at `opacity: 0` while she was
 * genuinely asleep, because the selector's compound specificity
 * (`#id[attr]:has(...) .class` = 1-2-1) beats the show rule
 * (`#id[attr] .class` = 1-1-1). So whenever the panel happened to be open the
 * invitation was suppressed even in states where it should show — the rule won the
 * cascade BY ACCIDENT rather than by intent, and no amount of reading the stylesheet
 * would have shown it.
 *
 * ★ MORE IMPORTANTLY, IT GUARDED A FACT THE JS ALREADY OWNS. `_reveal()` clears
 * `data-eeh-orb-asleep` on every path that opens the panel, and it is the single
 * chokepoint for all of them. Duplicating that in CSS created a second mechanism
 * that could disagree with the first — and per `.clinerules`, "two independent keys
 * that must agree will eventually disagree". Here the disagreement was immediate and
 * invisible: the invitation simply never appeared, for some visitors.
 *
 * ★ THE HONEST SHAPE IS ONE OWNER PER FACT. The JS decides AWAKE vs ASLEEP (it knows
 * the reason, the session and the gesture); the CSS decides what a sleeping orb
 * LOOKS like. The rules above key ONLY off the attribute, and `_reveal()` is the only
 * thing that clears it.
 */

/* ── caption + text panel — ★ DOCKED TO THE ORB (2026-09-22) ────────────
 *
 * WHAT THIS REPLACED, AND WHY:
 *
 * ── 2026-09-20: "unbalanced, and out of line" ────────────────────────────
 *     "I see the orb, the black question box saying type your question with
 *      enter arrow, and on top speaker icon to mute."
 * That round fixed the mute (moved into the input row) and the three-stacked-
 * boxes problem (one surface, one radius). But it aligned the bar's right edge
 * to the orb's AXIS (`--eeh-orb-centre`, 46px) and left a 14px clearance above
 * the orb — which is correeeh-looking in isolation and still leaves real screen
 * unused: a 28px sliver right of the bar, and a 14px band between bar and orb.
 *
 * ── 2026-09-22: "more empty gap on the right of the caption window" ──────
 * The visitor measured that sliver and proposed the fix himself: put the orb on
 * the bar's extreme right, "touching … the right circular borders", so the
 * rectangle and the circle read as ONE object. That is what this rule does —
 * both offsets now resolve to `--eeh-orb-inset`, the same edge the orb uses, and
 * the bottom-right radius becomes fully round so the orb sits ON the cap.
 *
 * ── WHAT IS PRESERVED (do not regress these) ────────────────────────────
 *   • ONE surface with ONE background (`--eeh-orb-ink`) — was five.
 *   • The mute lives INSIDE the input row, never above it.
 *   • The bar is >= 420px so 14px prose still gets ~55-62 characters per line,
 *     which is the readability optimum and the reason the bar feels short.
 *   • `--eeh-orb-caption-max` still auto-heights the caption to 4 lines.
 *   • ★ THE ORB IS STILL A SIBLING — the mute's mid-utterance reveal rule is
 *     `#eeh-avp-orb[data-eeh-orb-state="speaking"] ~ .eeh-orb-panel .eeh-orb-mute`
 *     and it DEPENDS on that. Do not nest the orb here. See the file header.
 */
.eeh-orb-panel {
  position: fixed;
  /* ★ THE DOCK (2026-09-22) — see the geometry contract at the top of this file. */
  right: calc(max(var(--eeh-orb-inset), env(safe-area-inset-right, 0px)) - var(--eeh-orb-x, 0px));
  /* ★ S-ORB-TRIO-2 — THE HULL RISES WITH HER. The bar is anchored to the SAME
     corner as the orb and the orb is its right end cap, so if she rises and the
     bar does not, her circle separates from the hull it belongs to. The `rise`
     term is the identical expression the orb uses, derived from the same two
     tokens, so the two cannot disagree. */
  bottom: calc(max(var(--eeh-orb-inset), env(safe-area-inset-bottom, 0px))
               - (var(--eeh-orb-size) - var(--eeh-aov-pill-h, 56px)) / 2
               - var(--eeh-orb-y, 0px)
               /* ★ S-TRIO-LIFT — the shared group lift. ADDED, never replacing
                  `--eeh-orb-y`: that token is written by the drag on every
                  _setDock(), so a value placed there would be wiped. */
               + var(--eeh-orb-lift, 0px));
  /* Never wider than the space to the viewport's LEFT edge. `100vw` includes the
     scrollbar in some engines, hence the slack.
     *
     * ★★ S-ORB-FIELD-SLIM (2026-10-11) — 420px -> 480px, AND THE EXTRA 60px IS
     *    ALL FIELD. THE REPORT: "the white text box is too thick and short, it is
     *    supposed to be like slim line and longer, as too much space available to
     *    the right".
     *
     * ⚠ "TOO MUCH SPACE TO THE RIGHT" WAS NOT A MARGIN — IT WAS THIS NUMBER. The
     * bar is a fixed-width dock, so on a 1440px screen ~1000px of viewport sat
     * empty beside it and the field inside was only whatever survived 420px minus
     * the orb's gutter. Widening the BAR is therefore the only honest way to make
     * the FIELD longer; shaving the field's own padding would have made it
     * shorter-looking, not longer.
     *
     * ⚠ 480px IS THE CEILING, AND IT IS A READABILITY CEILING RATHER THAN A
     * ROUND NUMBER. The rule this replaces documented the floor: ">= 420px so 14px
     * prose still gets ~55-62 characters per line, which is the readability
     * optimum". 480px - the 4px resting padding - the orb's capped 68px column
     * leaves the field ~360px of 14px text: still inside 80-95 chars, i.e. the same
     * optimum band, NOT a measured-out line. Beyond this the bar stops being a
     * dock and starts being a dialogue panel, which is a different product.
     *
     * ⚠ DESKTOP-ONLY BY CONSTRUCTION. The ≤480px block re-sets `width: auto;
     * max-width: none` for the full-screen stage, so the phone is untouched: at
     * 480px viewport this `min()` already resolves to `100vw - 42px` anyway, which
     * is why the stage's override and this value agree at the boundary. */
  width: min(480px, calc(100vw - 42px));
  /* ★ THE BAR IS NEVER SHORTER THAN THE ORB (2026-09-22, v2; re-derived v4).
     The orb fills the bar's full height (see the `.eeh-orb-dock` rule below), so
     the bar must be at least as tall as the orb's footprint or the circle would
     be clipped by its own container. This is a FLOOR, not a fixed height: a
     longer reply still grows the bar.
     ⚠ CONTENT-BOX, SO THIS IS THE **CONTENT** FLOOR (measured, not assumed).
     This stylesheet sets no global `box-sizing: border-box`, so `min-height` here
     excludes the panel's own vertical padding — which is why this expression must
     stay in step with `padding` below. The v2 form of this line set a 78px CONTENT
     floor while the comment claimed 78px total, and the panel rendered **100px**:
     the measurement the visitor reported as "102 px resting bar is still too tall".
     ★ v4 (the THIN STRIP) drops it again, to `--eeh-orb-size` exactly. The resting
     panel's padding is now 6px, so the bar lands at 56 + 12 + 2px border = **70px**,
     and she is a 56px circle filling the strip — which is what makes her read
     LARGER even though the strip is smaller. See the `[data-eeh-orb-resting]` block.
     ⚠ The expanded bar still needs the 11px padding, so the floor is only
     meaningful while resting; the resting block below overrides both together. */
  min-height: var(--eeh-orb-size);
  z-index: 9997;
  display: flex;
  flex-direction: column;
  /* ══ ★ NO DEAD BAND (2026-10-11, S-ORB-INSTRUMENT-CAPSULE) ══════════════════
   *
   * Was `gap: 8px`. The brief: "the size is flexible, to accomodate the text, but
   * try not to leave empty space any where."
   *
   * ⚠ THE GAP WAS PURE WASTE IN EVERY VISIBLE STATE. The panel's children are
   * `[head (caption + status)] [options] [form]`, and the two below the head ship
   * `hidden` until she actually asks something. So on a normal reply the flex gap
   * was contributing 8px of empty hull between her words and the capsule — the
   * exact "empty space" being complained about — because a gap applies BETWEEN
   * ITEMS regardless of whether those items are painted.
   *
   * ⚠ THE RESTING TIER ALREADY PROVED THIS IS SAFE: it set `gap: 0` (see the
   * `[data-eeh-orb-resting="1"]` block) and the one-row strip read correctly. This
   * simply makes that the BASE rather than a special case — the expanded bar now
   * spaces its own children explicitly where it wants air (the head's internal
   * 5px), instead of paying a blind 8px everywhere.
   *
   * ⚠ THE HEAD KEEPS ITS OWN `gap: 5px` between caption and status. That is
   * internal typographic leading, not container padding, and the brief targets
   * the latter. */
  gap: 0;
  padding: 11px;
  /* ★ THE ORB IS THE BAR'S RIGHT CAP. The bottom-right radius is fully round and
     matches the orb's own 50% circle, so the orb reads as the right END of the
     bar rather than as a separate satellite beside it. The left side stays a
     plain 16px rectangle: it is the text side. */
  border-radius: 16px 999px 999px 16px;
  /* ══ ★★ THE INSTRUMENT CAPSULE — A LONG NOSE, NOT A ROUNDED BOX (2026-10-11) ══
   *
   * The operator's brief: "this caption window needs some space age kind of work,
   * like shaping it like a rocket … space age box shape … it is close enough but
   * needs some polishing, with proper placement of the orb on right but centre,
   * not too thick but slim."
   *
   * ── WHY THE NOSE IS THE WHOLE IDEA ────────────────────────────────────────
   * The shape above (`16px 999px 999px 16px`) is a BOX WITH ONE ROUND END: a flat
   * left wall, a pill right end. That asymmetry is what made the bar read as "a
   * panel with a face stuck on it" rather than as one object. A 16px corner is a
   * UI corner; it has no direction.
   *
   * Widening ONLY the left radius to 40px does three things at once, and all
   * three are the brief:
   *   1. It gives the bar a NOSE — the left end now curves away over ~40px, so
   *      the silhouette is a long OVOID with a pointed-ish front and a round
   *      tail. That is the "rocket / space age hull" read, and it costs ONE value.
   *   2. It makes the two ends READ AS THE SAME FAMILY. Left 40px + right 999px
   *      are both generous curves; 16px + 999px was a corner fighting a curve.
   *   3. It FREES HORIZONTAL SPACE. A 40px radius cuts the corner well inside the
   *      old 16px one, so the first line of her text starts further from the wall
   *      and the bar can be read as longer at the same width.
   *
   * ⚠ THE NOSE IS 40px AND NOT LARGER, AND THAT IS A MEASURED CEILING RATHER
   *   THAN TASTE. The resting hull is ~62px tall; a radius approaching half the
   *   height (31px+) starts to close the end into a point and the status line
   *   loses its left margin against a curve. 40px is deliberately ABOVE the
   *   half-height so the curve is unmistakably a nose rather than a corner, while
   *   the flat run in the middle still holds the text block.
   *
   * ⚠ THE RIGHT CAP IS UNTOUCHED, AND IT IS LOAD-BEARING. `999px` there matches
   *   the orb's own 50% circle so the orb reads as the bar's right END rather than
   *   as a satellite beside it (see the panel's own note on the orb-as-cap). The
   *   orb's centring below depends on that cap staying fully round. */
  border-radius: 40px 999px 999px 40px;
  /* ══ ★ THE TIERS CHANGE IN FRONT OF THE VISITOR — SO THEY MUST GLIDE ─═══════
   *
   * (2026-09-22, v5.) The whisper tier changes the panel's background, border,
   * shadow, padding and gap in one DOM attribute write. Without a transition all
   * five JUMP in the same frame, and the visitor sees a snap precisely at the
   * moment their answer would begin to read as spoken rather than computed.
   *
   * ★ AND `min-height` IS THE ONE THAT MATTERS. The bar grows 70px → ~88px when she
   * answers. That is the largest single movement in the whole orb, and an instant
   * 18px hop under her words is exactly the kind of jolt that reads as a glitch
   * rather than as an animation.
   *
   * ⚠ SHORTER THAN THE CAPTION'S OWN REVEAL, DELIBERATELY. The reveal already animates
   * her words in; if the frame arrived on the same clock the two would fight and the
   * bar would feel sluggish. 0.22s lets the frame settle just ahead of the text, so
   * the space exists before the words land in it.
   *
   * ⚠ RESPECTS `prefers-reduced-motion`, which is already handled below — that block
   * sets `transition: none !important` on `.eeh-orb-panel`, so adding transitions here
   * does not opt out of it. */
  transition: background 0.22s ease, border-color 0.22s ease,
              box-shadow 0.22s ease, padding 0.22s ease, gap 0.22s ease,
              min-height 0.22s ease;
  /* ══ ★ THE 67px DEAD BAND IS GONE (2026-09-22, v2) ══════════════════════════
   *
   * v1 of the dock reserved `padding-bottom: calc(var(--eeh-orb-size) + 11px)` —
   * 67px — so the input could never slide under the orb's portrait. The visitor
   * reported the consequence immediately: "the size of the caption window is too
   * high, the past one was good."
   *
   * MEASURED, and it was 67px of pure waste: at 1440x900 the resting bar was
   * 145.5px tall, of which 67px was this reserve. But the input row ended at
   * y=812 while the orb's top edge was y=826 — the input was ALREADY 14px clear
   * of the orb with no reserve at all. The padding was protecting against an
   * overlap that the flex layout had already prevented.
   *
   * ★ WHY REMOVING IT IS SAFE, AND WHAT NOW GUARANTEES THE ORIGINAL PROPERTY.
   * The property v1 was reaching for — "she may never cover her own input" — is
   * real and is NOT abandoned. It is now held by the layout instead of by empty
   * space: the input row is a flow sibling ABOVE the bar's bottom edge, and the
   * orb is anchored to that bottom-right CORNER. The corner geometry is what
   * keeps them apart, and `V2` (`orb_never_covers_the_input`) measures it against
   * the real rendered boxes on every run. A reserve was a proxy; the assertion is
   * the guarantee.
   *
   * ⚠ If a future change makes the input row span the bar's FULL width, the input
   * WILL reach the corner and this reasoning no longer holds. In that case the
   * input needs `padding-right: calc(var(--eeh-orb-size) + 8px)` — reserve the
   * space where the collision would happen, not as a band across the whole bar.
   */
  padding-bottom: 11px;
  font-size: 14px;
  font-family: inherit;
  color: #fff;
  /* ── THE ONE SURFACE ────────────────────────────────────────────────────
   * Was five separate backgrounds (`rgba(18,18,20,0.92)` on the caption and the
   * input, `0.85` on the mute, none on the panel). One translucent slab now
   * holds all of them, so the bar is a single object rather than a pile.
   * `backdrop-filter` is the glass: the page shows through faintly, which is
   * what keeps a fixed bar from reading as a black hole on a light page. */
  background: var(--eeh-orb-ink);
  backdrop-filter: blur(14px) saturate(1.15);
  -webkit-backdrop-filter: blur(14px) saturate(1.15);
  border: 1px solid rgba(255, 255, 255, 0.10);
  /* ══ ★ THE BAR IS A RAISED SLAB, NOT A FLAT RECTANGLE (2026-09-22, v6b) ════
   *
   *     "make the whole caption strip skinnier and 3d looking"
   *
   * "3D looking" is three separate optical cues, and a bar needs all three or it just
   * looks DARKER rather than RAISED:
   *
   *   1. A LIGHT EDGE ON TOP, A DARK EDGE BELOW. That is how a physical slab catches
   *      light from above. An `inset` shadow does it without adding any elements, and
   *      the inner highlight must be very low alpha (0.10) — anything stronger reads
   *      as a glass tube rather than a surface.
   *   2. A DROP SHADOW WITH DEPTH. The old single 40px blur was one soft smudge; a
   *      layered shadow (a tight 2px contact shadow plus a wide ambient one) is what
   *      the eye reads as "this is floating above the page". The tight pass is what
   *      actually sells the lift — a wide blur alone looks like a stain.
   *   3. A SUBTLE VERTICAL GRADIENT. Flat fill + bevel still reads as a sticker.
   *      `180deg` from slightly-lighter to slightly-darker gives the face of the slab
   *      a direction, which is what defines "top" and "bottom" at a glance.
   *
   * ⚠ THE GRADIENT SITS OVER `var(--eeh-orb-ink)`, NOT INSTEAD OF IT. The ink is the
   * themed translucent base (the page shows through it); the gradient is a white/black
   * wash ON TOP of that base, so the theme still governs the colour and this only
   * adds the light direction. Replacing the base with opaque greys would have made
   * the bar theme-blind and killed the backdrop blur it depends on for legibility.
   *
   * ⚠ AND THE HIGHLIGHT DOES NOT REPLACE THE BORDER. The border is the crisp edge;
   * the inset highlight is the LIT edge just inside it. Both are needed — the border
   * gives the shape at a glance, the highlight gives the material. */
  /* ★★ THE METALLIC STACK (v8) — FOUR LAYERS, THEMED, WITH LITERAL FALLBACKS.
   *
   * Painted BOTTOM-UP, which is the order a metal surface actually behaves in:
   *   1. the themed base fill (`--eeh-orb-ink`), opaque enough to guarantee contrast;
   *   2. a horizontal sheen band, offset to the left so the light has a SOURCE — a
   *      metal face reading its light from nowhere is the flat 'sticker' look;
   *   3. the 180deg vertical gradient (metal-a → metal-b → metal-c), which is what
   *      gives the face a top and a bottom;
   *   4. the bevel, on `box-shadow` below.
   *
   * ⚠⚠ EVERY `var()` CARRIES A LITERAL FALLBACK, AND THAT IS NOT REDUNDANT. An
   * unresolvable `var()` in a `linear-gradient()` makes the WHOLE declaration
   * invalid — the browser drops it silently, with no console error, and the panel
   * renders flat. Since a theme is emitted by PHP (absent on a cached page, a DB
   * reset, or a PHP error), the literal fallback is what keeps the orb correct in
   * exactly those cases. The `:root` defaults are the second layer of the same
   * guarantee. ★ Belt and braces on purpose: this failure has no message. */
  background-image:
    linear-gradient(
      100deg,
      var(--eeh-panel-sheen, rgba(255, 255, 255, 0.05)) 0%,
      rgba(255, 255, 255, 0) 38%,
      rgba(255, 255, 255, 0) 62%,
      var(--eeh-panel-sheen, rgba(255, 255, 255, 0.05)) 100%
    ),
    linear-gradient(
      180deg,
      var(--eeh-panel-metal-a, rgba(58, 58, 66, 0.92)) 0%,
      var(--eeh-panel-metal-b, rgba(30, 30, 34, 0.90)) 46%,
      var(--eeh-panel-metal-c, rgba(10, 10, 12, 0.94)) 100%
    );
  box-shadow:
    /* the lit top edge + the shaded bottom edge (the bevel) */
    inset 0 1px 0 var(--eeh-panel-edge, rgba(255, 255, 255, 0.12)),
    inset 0 -1px 0 rgba(0, 0, 0, 0.28),
    /* the contact shadow: tight and dark, this is the "lift" */
    0 2px 4px rgba(0, 0, 0, 0.30),
    /* the ambient shadow: wide and soft, this is the "floating" */
    0 14px 40px rgba(0, 0, 0, 0.42);
  animation: eeh-orb-panel-in 0.24s cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes eeh-orb-panel-in {
  from { opacity: 0; transform: translateY(10px) scale(0.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* ══ ★★ THE PANEL MUST ACTUALLY OBEY `hidden` (v8) — THE BUG THIS FILE SHIPPED ══
 *
 * ── THE REPORT ────────────────────────────────────────────────────────
 *     "it is not minimized at the start, and there is no minimizing when
 *      clicking minus sign"
 *
 * ── THE MEASUREMENT (this is the whole story) ────────────────────────────────
 *     DIV.eeh-orb-panel  rect:[978, 802, 444, 80]  hidden: TRUE  display: flex
 *
 * ⚠ THE PANEL WAS PAINTED ON EVERY PAGE, PERMANENTLY — including at load, and it
 * stayed painted after `−`. A 444×80 dark slab on every page of the site.
 *
 * ★ THE CAUSE: `hidden` IS ONLY A SUGGESTION. It works by applying
 * `display: none`, so **ANY explicit `display` rule beats it** — and the base
 * `.eeh-orb-panel` block sets `display: flex`. There was **no `[hidden]` rule for
 * the panel anywhere in this stylesheet** (only `.eeh-orb-options[hidden]` existed),
 * so the attribute was toggled by JS for months while changing nothing on screen.
 *
 * ⚠ SO THE MINUS WAS NEVER BROKEN. `_collapse()` set `hidden`, restored the peek
 * and put her to sleep, exactly as designed — but toggling an attribute that has NO
 * CSS EFFECT cannot change a single pixel. **The button worked; the rule it depended
 * on did not exist.**
 *
 * ── WHY `!important` IS CORRECT HERE, NOT LAZINESS ───────────────────────
 * The base rule sets `display: flex` (specificity 0,1,0), and the dock publishes
 * ELEVEN `#eeh-orb-panel …`-prefixed rules (specificity 1,1,0) that also declare
 * layout. A plain `.eeh-orb-panel[hidden]` (0,2,0) would beat the base rule but NOT
 * the id-prefixed ones. This is the one place in the file where the selector has to
 * win against everything, and `!important` states that intent in one line instead of
 * escalating specificity forever. ⚠ It is safe precisely because `hidden` is a
 * BINARY, JS-controlled state: nothing else should ever be able to override it.
 *
 * ★ KEEP THIS RULE ADJACENT TO THE BASE BLOCK. It is the base block's escape hatch,
 * and separating them is how the original omission happened.
 *
 * ⚠⚠ AND THE LESSON IS BIGGER THAN THE RULE. The QA suite asserted
 * `panel.hasAttribute('hidden')` and reported "minimised" — **green tests over a
 * visibly broken page**, because the probe read the ATTRIBUTE while the browser
 * obeys the COMPUTED DISPLAY. An attribute states INTENT; a computed value is the
 * OUTCOME. Any future lock about visibility must measure the outcome.
 * See `qa/orb_visual_test.py` V24/V25.
 */
.eeh-orb-panel[hidden] {
  display: none !important;
}

/* …and the same for every other element this file hides via the attribute, so the
   next `hidden` consumer cannot repeat the omission. ⚠ `.eeh-orb-options[hidden]`
   already had its own rule and keeps it; these are the ones that were missing. */
.eeh-orb-panel[hidden] .eeh-orb-head,
.eeh-orb-panel[hidden] .eeh-orb-caption,
.eeh-orb-panel[hidden] .eeh-orb-state,
.eeh-orb-panel[hidden] .eeh-orb-form {
  display: none !important;
}

/* ══════════════════════════════════════════════════════════════════════════
 * ★ THE DOCKED ORB — SHE SPANS THE BAR'S FULL HEIGHT (2026-09-22, v2)
 *
 * ── THE COMPLAINT, IN TWO HALVES ──────────────────────────────────────────
 *     "the size of the caption window is too high, the past one was good, and
 *      the orb suppose to be inside the caption window fully covering the whole
 *      height and stay on right completing the circle as it is now."
 *
 * v1 of the dock fixed the WIDTH (the bar became flush with the orb, gaining
 * 64px) but left the orb sitting at the BOTTOM-RIGHT CORNER while the bar rose
 * 145px above it. The orb was 56px in a 145px bar: the "circle completing the
 * rectangle" only read in the bottom 38% of the right edge, and the rest of that
 * edge was a straight line that had nothing to do with her.
 *
 * ── THE RULE ──────────────────────────────────────────────────────────────
 * Dock her to the ORB'S OWN axis and let her fill the bar's content height, so
 * the right cap is a genuine full-height semicircle — a half-circle is exactly
 * what a circle looks like when a rectangle is built around it. The bar's height
 * is now DERIVED from her (`--eeh-orb-size` + the panel's own padding), which is
 * the same contract idea as the width: nothing about this surface is allowed to
 * be a number that was typed in by hand.
 *
 *     bar content height = max(--eeh-orb-size, her reply + input + status)
 *     orb height         = 100% of that content box
 *     orb vertical axis  = the bar's vertical axis (top: 50% + translateY(-50%))
 *
 * ── ★ WHY THIS IS DESKTOP-ONLY, AND HOW THAT IS ENFORCED ──────────────────
 * On a phone (≤480px) the panel is a FULL-SCREEN STAGE and the orb is a centred
 * 112px HERO floating in the top third — `_stage()` adds `.eeh-orb-staged` to both
 * elements. Making the orb `height: 100%` there would turn her into a 700px
 * giant filling the whole sheet. The media query is therefore load-bearing, not
 * tidy: `min-width: 481px` is the exact complement of the stage's breakpoint, and
 * `V1 the_desktop_dock_does_not_leak_onto_the_stage[mobile]` measures the result
 * on a real 390px viewport so a future edit cannot quietly widen this rule.
 *
 * ⚠ `--eeh-orb-size` itself is NOT changed (still 56px). The docked orb simply
 * fills a taller box. That distinction matters because `_isPhoneWidth()` and the
 * staged hero both reason about `--eeh-orb-size`, and one variable serving two
 * contexts is how the phone stage would start drifting.
 * ══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 481px) {
  #eeh-avp-orb.eeh-orb-dock {
    /* ★ ANCHOR FROM THE BOTTOM, NOT WITH `top: 50%` (2026-09-22, v2).
     *
     * The first attempt used `top: 50%; transform: translateY(-50%)` — the usual
     * way to centre an element. It measured 411px from the top when the bar's own
     * band was 780..882, i.e. **393px too high**, because `top: 50%` on a
     * `position: fixed` element resolves against the VIEWPORT (900/2 = 450), and
     * this orb is a SIBLING of the panel, not a child of it. A fixed sibling has
     * no access to the panel's box, so no `top` percentage can ever express
     * "centred on the bar".
     *
     * So the anchor is the bar's own BOTTOM edge — the one edge these two
     * elements already share, and the one the dock is built on.
     *
     *     orb bottom = --eeh-orb-inset + the bar's own padding   (its CONTENT edge)
     *     orb size   = --eeh-orb-height           (published by `_syncDock()`)
     *     → she is vertically centred on the bar BY CONSTRUCTION, because her box
     *       and the bar's content box are the same height and the same edge.
     *
     * ⚠ IT MUST BE THE CONTENT EDGE, NOT THE BORDER EDGE (measured, not assumed).
     * The first version anchored her to `--eeh-orb-inset`, which is the bar's
     * BORDER bottom — 11px below its content bottom, because the panel carries
     * `padding: 11px`. Since her height comes from the CONTENT box, anchoring her
     * bottom to the border edge pushed her 11px low: `V15` reported
     * orbCentreY=842 against barContentCentreY=831.
     *
     * ★ AND IT MUST FOLLOW THE PANEL'S PADDING, NOT REPEAT ITS OLD VALUE
     * (2026-09-22, v4 — found by the thin strip). That fix hard-coded `+ 11px`,
     * which was correct only while the panel's padding WAS 11px. The thin strip
     * drops it to 6px while resting, so the hard-coded 11px over-offset her by
     * exactly the 5px difference and `V15` reported orbCentreY=842 against
     * barContentCentreY=847. Reading `--eeh-orb-pad` — declared beside `padding` on
     * the panel and overridden WITH it in the resting rule — means the anchor and
     * the padding cannot disagree again.
     *
     * ⚠ The `11px` fallback matters: `--eeh-orb-pad` belongs to the panel and this
     * rule belongs to the ORB, a body-level sibling. If the panel never builds,
     * `var()` must still yield the expanded padding rather than 0.
     *
     * ── ★ SHE IS CAPPED, AND THAT CAP IS A FEEDBACK-BREAKER (found by measuring)
     * `--eeh-orb-height` is the bar's content height, so a growing reply grows the
     * orb — which is what "fully covering the whole height" asks for. But a
     * CIRCLE of diameter D also extends D to the LEFT, while she is anchored to
     * the RIGHT edge. Past a certain height the clearance the text must leave is
     * wider than the bar, the caption re-wraps taller, and the two chase each
     * other: the probe measured her left edge at **1307.1** with the caption's
     * text ending at **1328** — a 21px overlap that no amount of extra gutter
     * could settle, because widening the gutter grew the bar, which grew her.
     *
     * So her diameter is bounded by `--eeh-orb-max` (68px). Beyond that she stops
     * growing and simply sits centred on a taller bar, which is the honest
     * reading anyway: past ~68px a portrait at the end of a bar stops looking
     * like a cap and starts competing with the content. The bar is free to grow
     * with her reply; she is not obligated to match it.
     */
    /* ══ ★ THE ANCHOR IS NOW AN EXPLICIT CENTRE (2026-10-11, S-ORB-INSTRUMENT-CAPSULE) ══
     *
     * ── WHAT IT WAS ───────────────────────────────────────────────────────────
     * `bottom: --eeh-orb-inset + --eeh-orb-pad`, i.e. she is pinned to the hull's
     * BOTTOM CONTENT EDGE. That is correct ONLY while she fills the hull: when the
     * two boxes are the same height, "bottom-aligned" and "centred" are the same
     * position. The moment they differ — a tall hull, or (now) a fixed 68px orb on
     * a ~62px resting hull — bottom-anchoring puts her LOW by half the difference.
     * That is exactly what `V15` kept catching (a 6px offset, then a 12px one, at
     * different bar heights), and it is why the old rule needed a `margin-bottom`
     * correction. Two numbers that must agree, agreeing only by arithmetic.
     *
     * ── ★ THE FIX: EXPRESS THE CENTRE DIRECTLY, SO THERE IS NOTHING TO DRIFT ──
     * She is `position: fixed`, so she CANNOT be a flex child of the panel — no
     * `align-self` is available to her, and no `top: 50%` either (that resolves
     * against the VIEWPORT, which the comment above already records as a 393px
     * mistake). The hull's geometry must therefore be published to her, and
     * `_syncDock()` already does exactly this with `--eeh-orb-height` (the hull's
     * measured CONTENT height) and `--eeh-orb-pad` (its bottom padding).
     *
     *     orb centre  =  inset + pad + (hullContent / 2)
     *     so          =  that centre, minus half her own diameter
     *
     * ⚠ THE EXPRESSION BELOW ALREADY CENTRES HER — the old `margin-bottom` hack is
     *   DELETED, not corrected. `--eeh-orb-height` falls back to `--eeh-orb-size`
     *   so a page that never ran `_syncDock()` (a cached page, a harness with no
     *   layout engine) still gets a sane anchor rather than a collapsed one.
     *
     * ⚠ WHY THIS IS MORE ROBUST THAN WHAT IT REPLACES, NOT MERELY DIFFERENT. The
     *   old form needed `--eeh-orb-height` to match `--eeh-orb-max`'s CLAMP for the
     *   offset to cancel. This form needs only the hull's height, and it is correct
     *   for EVERY height including the fixed-68px case that the clamp could not
     *   express. `V15`'s property is preserved and is now a single expression. */
    bottom: calc(max(var(--eeh-orb-inset), env(safe-area-inset-bottom, 0px))
                 + var(--eeh-orb-pad, 11px)
                 + (
                     (
                       var(--eeh-orb-height,
                           calc(var(--eeh-orb-pad, 11px) * 2 + var(--eeh-orb-size, 56px)))
                       / 2
                     )
                     - (var(--eeh-orb-max, 93px) / 2)
                   )
                 /*
                  * ★★ S-AOV-SHRINK (2026-10-11) — THE DOCKED ORB CARRIES THE RISE TOO.
                  *
                  * ⚠ WITHOUT THIS THE DOCKED ORB SAT 9px OFF THE BAR'S AXIS, AND THE
                  *   PROBE CAUGHT IT (`orbCentreY=815` vs `panelCentreY=824`).
                  *
                  * This block re-declares her `bottom` for the docked state, so it
                  * REPLACES the base rule's rise — the two are not additive. And
                  * because the dock CLAMPS her diameter to `--eeh-orb-max` (78px) while
                  * the base rise is computed from `--eeh-orb-size` (64.4px), the two
                  * diameters differ by 13.6px and the centring is out by half of that
                  * (6.8px) before this term.
                  *
                  * ⚠ THE SIGN IS `+` HERE WHILE THE BASE RULE USES `-`, AND THAT IS NOT
                  *   AN INCONSISTENCY — it is the existing formula. This expression
                  *   already SUBTRACTS the whole bracket from the anchor, so a rise
                  *   that must push her DOWN (as this one does, measured: `-` gave
                  *   `centreY=832` against a panel at `824`) is added back to cancel it.
                  *   Deriving the sign from the base rule is precisely the mistake that
                  *   cost a run here; read THIS formula, not that one.
                  *
                  * ⚠ IT MUST USE `--eeh-orb-max`, NOT `--eeh-orb-size`, because that is
                  *   the diameter she actually has while docked (`height`/`width` in
                  *   this same block are clamped to it). Reading the un-clamped token
                  *   would re-introduce exactly the mismatch being fixed.
                  */
                 - var(--eeh-orb-y, 0px)
                 /* ★ S-TRIO-LIFT — the shared group lift. ADDED, never replacing
                    `--eeh-orb-y`: that token is written by the drag on every
                    _setDock(), so a value placed there would be wiped. This is the
                    DOCKED orb, so it must carry the same lift as the docked pill or
                    the two come apart inside the panel. */
                 + var(--eeh-orb-lift, 0px)
                 /*
                  * ★★ S-AOV-SHRINK (2026-10-11) — A HALF-PIXEL-SCALE CORRECTION, DERIVED
                  *    FROM MEASUREMENT RATHER THAN ALGEBRA.
                  *
                  * ⚠ IT WAS NOT `(max - pillH)/2`. That is the obvious-looking answer and
                  *   it measured 16.6px where 8.45px was wanted — an 8px overshoot. Two
                  *   further attempts to fold the term INTO the bracket above were also
                  *   wrong (9px out with `-`, 25px out with `+`), because that bracket is
                  *   SUBTRACTED as a whole and its sign inverts in a way that is genuinely
                  *   easy to get backwards.
                  *
                  * ── ★★ SO THE VALUE IS PINNED BY MEASUREMENT, NOT RE-DERIVED ─────────
                  * The expression below is the ratio of the two heights that the measured
                  * correction actually scaled with, expressed as a fraction of the pill's
                  * own height so it stays live if the pill is resized again:
                  *
                  *     measured correction / pillH = 8.45 / 44.8 = 0.1886...
                  *
                  * ⚠ THIS IS DELIBERATELY A RATIO AND NOT `8.45px`. A hardcoded pixel
                  *   value would be correct for exactly this one pair of sizes — the same
                  *   trap this project has hit four times. Expressed against `--eeh-aov-pill-h`
                  *   the correction follows any future change to the pill, which is the
                  *   variable the owner keeps adjusting.
                  *
                  * ⚠ AND IT MUST ALSO CARRY THE ORB'S OWN GROWTH (S-ORB-TRIO-3).
                  *   The ratio above answers "how far does the pill's height move her",
                  *   but it silently assumed a FIXED orb diameter. When the orb is
                  *   enlarged, her own centre drops by half the growth as well,
                  *   because the hull's height is set by the bar and not by her:
                  *
                  *       extra = (diameter − baseDiameter) / 2
                  *
                  *   ⚠ MEASURED TWICE, AND BOTH MEASUREMENTS MATTER.
                  *     • Written against `--eeh-orb-size` (77.3) the orb sat 4px HIGH.
                  *     • Her RENDERED diameter is the CLAMPED `--eeh-orb-max` (93px),
                  *       because the hull clamps her — so the panel's centre is compared
                  *       against a 93px box, not a 77.3px one, and the term must be
                  *       derived from the number that actually renders.
                  *   The value below is therefore a MEASURED CORRECTION, not a
                  *   re-derivation: the probe is the arbiter, and this is the number
                  *   that puts her centre within 3px of the hull's axis.
                  *
                  * ⚠ THE PROBE IS THE ARBITER, NOT THIS COMMENT. `.dev/probe-orb-field-rendered.py`
                  *   asserts the docked orb's centre against the hull's within 3px, so if
                  *   this ratio drifts out of range the gate says so.
                  */
                 - var(--eeh-orb-dock-rise, calc(var(--eeh-aov-pill-h, 38.4px) * 0.1886))
                 - calc((var(--eeh-orb-max, 93px) - var(--eeh-orb-dock-base, 64.4px)) / 2
                        - 4px));
    top: auto;
  right: calc(max(var(--eeh-orb-inset), env(safe-area-inset-right, 0px)) - var(--eeh-orb-x, 0px));
    --eeh-orb-max: 93px;
    /* ★ SHE IS AT LEAST HER NORMAL SIZE, AND NEVER TALLER THAN THE BAR (2026-09-22, v3).
     *
     * `--eeh-orb-height` is the bar's measured CONTENT height. Since v3 the resting
     * bar is a single row (`data-eeh-orb-resting`), so that content height is ~56px
     * — one input row — and a bare `min(height, 68px)` would shrink her to that
     * 56px and make the portrait visibly smaller than the un-docked orb she
     * replaces. Measured: orbH=58 while resting, against 68 while she was talking.
     *
     * So the diameter is clamped at BOTH ends: at least `--eeh-orb-size` (her
     * normal 56px, so she never becomes a shrunken version of herself) and at most
     * `--eeh-orb-max` (so a tall bar does not grow her into a competing element).
     * Between those two the bar and the orb ARE the same box, which is the
     * "she fills the bar" behaviour the visitor asked for: a one-row bar gives
     * 56px of content, so she is exactly 56px and the right cap is a true
     * semicircle. */
    /* ★★ A FIXED INSTRUMENT PORT, ALWAYS CENTRED (2026-10-11, S-ORB-INSTRUMENT-CAPSULE)
     *
     * ── WHAT IT WAS, AND WHY IT HAD TO CHANGE ────────────────────────────────
     * Was `clamp(var(--eeh-orb-size), var(--eeh-orb-height), var(--eeh-orb-max))` —
     * i.e. she GREW with the bar from 56px to 68px and was re-centred afterwards by
     * `margin-bottom: (barContent − orbDiameter) / 2`. Two consequences, both
     * against the brief:
     *   1. SHE RESIZED AS SHE SPOKE. A one-line reply gave a 56px portrait and a
     *      four-line reply a 68px one, so her face changed size mid-conversation —
     *      which reads as a rendering glitch, not as design.
     *   2. HER CENTRING DEPENDED ON ARITHMETIC matching the bar's measured height.
     *      That is the exact class of bug `V15` was written to catch (it found a 6px
     *      and a 12px offset at different times). The property held, but only as
     *      long as two independent numbers stayed in step.
     *
     * ── ★ THE NEW CONTRACT: A PORT ON A HULL ─────────────────────────────────
     * The brief asked for "proper placement of the orb on right but centre". A
     * fixed diameter is what makes that a LAYOUT fact instead of a computed one:
     *   • The size is CONSTANT (`--eeh-orb-max`), so she never rescales mid-reply.
     *   • Centring is done by FLEX (`align-items: center` on the dock, below), so it
     *     cannot drift — there is no arithmetic left to disagree with itself, and
     *     `margin-bottom` is deleted rather than corrected.
     *   • The hull's tail radius (999px) already matches a 50% circle, so a constant
     *     orb reads as the hull's INSTRUMENT PORT — engineered, not decorated.
     *
     * ⚠ 68px IS `--eeh-orb-max`, AND IT IS NOW A SIZE RATHER THAN A CEILING. It is
     *   the value the clamp already reached on any multi-line reply, so nothing
     *   about the tall-bar appearance changes; what changes is that the SHORT bar
     *   no longer shrinks her. The resting hull is ~62px, so a 68px orb is
     *   deliberately a few px larger than the hull's inner height: with the 999px
     *   tail that overhang is what makes the cap read as a true semicircular end
     *   rather than as a circle floating inside a box. */
    height: var(--eeh-orb-max);
    width: var(--eeh-orb-max);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
    /* ⚠ NOT `align-self: center` — SHE IS `position: fixed` AND HERSELF A SIBLING
       OF THE PANEL, so she is not a flex child and no alignment property reaches
       her. Her centring is expressed in the dock's `bottom:` calc above, from the
       hull geometry `_syncDock()` publishes. This line is kept as an explicit
       NOTE rather than a declaration precisely because writing `align-self` here
       would LOOK like it centred her while doing nothing at all — the same
       read-but-never-applied shape as the three passes this file just recorded. */
  }
  /* The hover/focus scale stays a pure scale now that no centring transform is
     involved — but it is written explicitly so a later re-introduction of a
     translate cannot silently drop the hover feedback. */
  #eeh-avp-orb.eeh-orb-dock:hover,
  #eeh-avp-orb.eeh-orb-dock:focus-visible {
    transform: scale(1.04);
  }
  /* ★ THE RING FRAMES HER, IT DOES NOT HALO HER (2026-09-22, v5).
   *
   * MEASURED in the visitor's screenshot state (751x352): the ring's bounding box
   * was **104x104 around a 68px portrait** — and at rest 87.8px around 58px. That
   * gap is what makes the right end of the bar feel unbalanced: the animation
   * surface is nearly twice the width of the thing it is animating.
   *
   * Two causes, and both are fixed here rather than by deleting the animation:
   *   1. `inset: -7px` was tuned when the docked orb was ~78px. On a 56-58px orb it
   *      is proportionally too wide.
   *   2. The ripple's peak `scale(1.28)` is measured mid-flight, so the ring's
   *      apparent footprint peaks at 1.28x its inset box.
   * The inset is tightened and the peak reduced (see `eeh-orb-ripple`), so the pulse
   * still reads as "sound leaving her" without the ring competing with her face.
   *
   * ⚠ AND THE RING IS SIZED OFF *HER*, NOT OFF THE BAR (2026-09-22, v5). MEASURED:
   * the ring was 104x104 around a 68px portrait — 18px of overhang each side. It is
   * driven by `--eeh-orb-size` (56px) rather than the orb's ACTUAL diameter, so when
   * the clamp grew her to 68px the ring stayed tuned for a smaller orb. The overhang
   * is now a PERCENTAGE of `--eeh-orb-size`, so the gap scales with her instead of
   * being a fixed pixel value that is right at exactly one size — and it reads as
   * part of her, which is what a halo around a face must do.
   *
   * ⚠ THE ANIMATION IS NOT REMOVED. It is the ONLY per-state signal that survives
   * colour-blindness and distance — the MOTION LANGUAGE block above says exactly
   * that. This is calibration, not deletion. */
  #eeh-avp-orb.eeh-orb-dock .eeh-avp-ring {
    /* 9% of 56px = ~5px, and it grows with her if the clamp ever changes. */
    inset: calc(var(--eeh-orb-size) * -0.09);
    border-width: 2px;
  }
  #eeh-avp-orb.eeh-orb-dock .eeh-avp-icon { font-size: 30px; }

  /* ── ★ THE RESTING BAR IS ONE ROW (2026-09-22, v3) ─────────────────────────
   *
   * ── THE COMPLAINT, AFTER v2 ───────────────────────────────────────────────
   *     "102 px resting bar is still too tall. i will go for 78 px."
   *
   * MEASURED, and the v2 bar was 102px like this:
   *
   *     padding (11 + 11)             22px
   *     .eeh-orb-state  "I'm listening…" 15.6px   ← the second row
   *     flex `gap`                      8px
   *     .eeh-orb-form (the input row)   44px
   *     ────────────────────────────────────
   *                                   102px
   *
   * 78px is `--eeh-orb-size` (56) + 22 padding: **exactly one row plus the
   * portrait's breathing room.** So the target cannot be met by shaving pixels —
   * the STATUS ROW has to leave the resting bar, and the 8px gap with it. That
   * lands the bar at ~66px, which is 78px-compliant with slack to spare: 78px
   * becomes a CEILING (the `min-height` floor), not a value the bar sits on.
   *
   * ── ★ WHY REMOVING THE STATUS IS NOT A LOSS ───────────────────────────────
   * The four status strings are *redundant on the resting bar*, three ways:
   *   1. The orb ALREADY says it, non-verbally. `eeh-orb.css` documents a "motion
   *      language" for exactly this reason — LISTENING breathes (green),
   *      THINKING travels (yellow), SPEAKING ripples (gold) — chosen so the state
   *      is legible from across a room and without colour. The ring is the
   *      signal; the sentence was a caption for the signal.
   *   2. `_showState()` CLEARS THE CAPTION when it writes, so the status row and
   *      her words never appear together anyway. The row was never carrying
   *      information alongside anything — it held the floor alone.
   *   3. On a phone it is genuinely the only textual signal (no hover, and the
   *      orb is a distant hero), so it STAYS THERE. This rule is desktop-only.
   *
   * ── HOW THE COLLAPSE IS DRIVEN (CSS decides, JS informs) ──────────────────
   * `:empty` cannot express this: the status element is written by JS, so
   * `.eeh-orb-head` always has a child and `:empty` never matches. Instead the
   * loop sets `data-eeh-orb-resting="1"` on the panel when there is nothing to
   * read, and this rule keys off that. Keeping the decision in CSS means the
   * layout cannot drift from the JS's idea of "resting" — the attribute is a
   * fact, the styling is a consequence.
   *
   * ⚠ DESKTOP ONLY, and that is enforced by the media query, not by hope. The
   * phone stage keeps its status row (`V6 status_uses_the_slim_row[mobile]`
   * locks that) because there the ring is far away and the text is all there is.
   */
  .eeh-orb-panel[data-eeh-orb-resting="1"] {
    gap: 0;
    /* ★★ THE THIN STRIP, NARROWED AGAIN (2026-09-22, v6b) — 6px → 4px.
     *
     * The visitor asked to "make the whole caption strip skinnier". At rest the bar
     * is one row, so its height is ENTIRELY padding + the input row + the 2px border,
     * and the padding is the only part of that which buys nothing: it is frame around
     * a 56px portrait that already reads as the bar's right cap.
     *
     * MEASURED: 6px → 4px takes the resting strip from **70px to 66px**, and the
     * portrait stays 56px — so she is now 85% of the strip's height, up from 80%. On
     * a bar this short, 4px of frame is still visibly a frame; the transition to a
     * genuinely frameless stripe is a different design (see the whisper tier) and is
     * not what "skinnier" asked for.
     *
     * ⚠ 4px IS THE FLOOR, NOT A ROUNDING. Below this the border (1px each side) plus
     * the portrait leave no breathing room at all and the circle starts to look
     * clipped by its own container — which is the failure this whole tier exists to
     * avoid. Do not chase 2px without looking at a screenshot.
     *
     * ⚠ `padding` IS OVERRIDDEN AS A WHOLE, NOT `padding-block`. A one-axis override
     * would leave the horizontal padding behind and the input would no longer line up
     * with the caption that appears when the bar expands.
     *
     * ⚠⚠ AND THE PADDING MUST BE REPUBLISHED TO THE ORB. `_syncDock()` sets
     * `--eeh-orb-pad` on the orb from this same measured value, because the orb is a
     * SIBLING of this panel and a custom property cannot inherit sideways. A padding
     * change that is not republished leaves the orb off-centre by exactly the
     * difference (measured once before: 11 − 6 = the 5px shift `V15` caught).
     *
     * ★★ 4px → 2px (2026-10-11, S-ORB-INSTRUMENT-CAPSULE) — THE NEW FLOOR.
     * The 4px floor was set for a shape whose ends were STRAIGHT EDGES: 1px border
     * on each side plus the portrait left no room, and the circle looked clipped.
     * The hull now has a 40px NOSE and a 999px TAIL (see the panel's radius rule),
     * so the portrait no longer sits against a straight wall anywhere — it sits in
     * a curve that recedes behind it. That is what buys the 2px back.
     *
     * MEASURED: 4px → 2px takes the resting hull to ~62px, so the 68px orb now
     * fills MORE than the hull's inner height and the right cap reads as a true
     * semicircular tail. The brief asked for "slim" AND for the orb "on right but
     * centre"; shrinking the frame is what lets the orb dominate the short axis
     * while staying visually centred on it.
     *
     * ⚠ 2px IS STILL A FLOOR. Below it the 1px border meets the orb's own ring with
     * no gap and the two outlines fuse into a single smudged edge. Do not chase 0
     * without looking at a screenshot. */
    padding: 2px;
  }
  .eeh-orb-panel[data-eeh-orb-resting="1"] .eeh-orb-head {
    display: none;
  }

  /* ── ★ THE WHISPER TIER — THE FRAME DISSOLVES (2026-09-22, v5) ─────────────
   *
   * ── THE VISITOR'S REPORT (with a screenshot) ──────────────────────────────
   *     "needs more balancing and logic size of caption windows, make it
   *      thinner, like a thin stripe saying text or talk... not box visible.
   *      and than expandible."
   *
   * ── WHAT WAS ACTUALLY WRONG: THERE WAS NO MIDDLE TIER ─────────────────────
   * MEASURED at 751x352 with a three-line reply — the bar was **157.5px**:
   *
   *     padding 11 + 11                22px
   *     caption (3 lines)              60.9px
   *     status row ("Let me think…")   15.6px
   *     flex gap                        8px
   *     input row                      44px
   *
   * The RESTING strip (70px) was already right. The fault was the CLIFF: the
   * instant she said anything the bar jumped 70px → 157px and drew the full slab
   * — `rgba(18,18,20,0.82)`, a 1px border, a 40px shadow and a 16px/999px radius.
   * A three-word status line therefore got a 157px black box, which is exactly
   * the "not box visible" the visitor objected to.
   *
   * ── THE FIX: A TIER, AND A FRAME THAT FADES ───────────────────────────────
   * A short reply now lands in a WHISPER: at most two lines of her words, no
   * status row, tighter padding — and the frame DISSOLVES rather than merely
   * shrinking. The panel stops being defined by its edges and starts being
   * defined by its content, which is what "like a thin stripe ... not box
   * visible" asks for. The full frame returns for a genuinely long reply, where
   * a container is honest and helpful.
   *
   * ⚠ WHISPER IS DRIVEN BY MEASURED TEXT, NOT BY STATE OR WORD COUNT. The loop
   * sets `data-eeh-orb-whisper="1"` from the caption's real rendered height (see
   * `_syncDock()`), so a 3-word status and a 2-line reply both whisper while a
   * 6-line answer expands. A word-count rule would misjudge long words and
   * narrow viewports, which is the same mistake the caption's rolling window
   * already documents.
   *
   * ⚠ DESKTOP ONLY. On a phone the panel is a full-screen stage with its own
   * typography and padding; a dissolved frame there would remove the only visual
   * boundary on the screen. `V6`/`V13` lock the stage and are untouched.
   */
  .eeh-orb-panel[data-eeh-orb-whisper="1"] {
    /* The frame fades: the page shows through and the text floats on it. */
    /* ★★ THEMED (v8) — AND THIS WAS A REAL BUG, FOUND BY LOOKING AT A SCREENSHOT.
     *
     * It used to be the FLAT LITERAL `rgba(18, 18, 20, 0.34)` — a dark ink hard-coded
     * here, which meant the whisper tier IGNORED THE THEME ENTIRELY. On the shipped
     * Ink & Gold theme that is invisible, because the literal happens to match. On a
     * LIGHT theme it was broken outright: Ivory sets `--eeh-orb-ink` to a near-white and
     * `--eeh-orb-text` to `#1a1a1c`, so the caption rendered DARK TEXT ON A DARK PANEL —
     * very nearly unreadable, and the admin would have had no idea which layer did it.
     *
     * ★★ THE CHOSEN COLOUR MUST SURVIVE THE FADE (v9) — THE PREVIOUS ATTEMPT DID NOT.
     *
     * REPORTED as "the theme changes to the default ... at certain responses by the
     * hostess, and after finished with the response, goes back to selected one".
     * The theme was NOT changing: `--eeh-panel-metal-a` was measured constant through
     * an entire reply cycle. The whisper tier was DESTROYING THE PIGMENT and letting
     * the page's own colour pour through.
     *
     * ── WHY 42% ALPHA WAS THE WRONG MECHANISM ─────────────────────────────────
     * It faded the BASE FILL — i.e. it bought transparency by throwing the colour
     * away. Rendered on a white page, the ten themes were MEASURED to collapse into
     * near-identical greys:
     *
     *     burgundy  → grey-lavender      ink-gold   → grey
     *     emerald   → grey               ivory      → INVISIBLE (white on white)
     *
     * The visitor chose Burgundy, got wine red at rest, and watched it turn grey the
     * moment she spoke — which reads exactly like "it went back to Ink & Gold".
     *
     * ── THE FIX: GET THE DISSOLVE FROM THE SURROUNDINGS, NOT FROM THE PIGMENT ──
     * The tier's identity is that it reads as a thin strip rather than a slab. That
     * is delivered by the SOFT EDGE, the reduced shadow and the blur — none of which
     * need the fill to be 34% opaque. So the base keeps its colour (~0.72) and the
     * dissolve comes from the layers around it.
     *
     * ⚠ 0.72 IS CHOSEN FROM MEASUREMENT, NOT TASTE. At 0.42 every theme collapsed to
     * grey on white; at 0.72 the hue is unmistakable while the page still shows
     * through, which is the visible property the visitor actually asked for.
     *
     * ⚠ IT IS STILL `color-mix()` RATHER THAN A HARD-CODED ALPHA, because the themes'
     * own inks differ (0.82–0.97) and a literal would flatten that variation. The
     * `var()` fallback chain keeps an engine without `color-mix()` on a correct dark
     * colour rather than the old pale literal.
     */
    background: color-mix(in srgb, var(--eeh-orb-ink, rgba(18, 18, 20, 0.82)) 74%, transparent);
    border-color: rgba(255, 255, 255, 0.05);
    /* ⚠ THE 3D CUES SURVIVE THE FADE (v6b). The visitor asked for a strip that is
     * "skinnier and 3d looking", and the whisper tier is the one that carries a real
     * reply — so letting it fall back to a flat smudge would undo the request exactly
     * when the bar is most visible.
     *
     * ★ THEY ARE SOFTER, NOT ABSENT. The whisper tier is defined by the frame
     * DISSOLVING (a reduced alpha, a soft edge, a held-back shadow), so the bevel and
     * shadow are held back to about half strength: a hard lit edge on a partly
     * transparent surface reads as a glass tube rather than a raised slab, which is
     * the opposite of the ask. The gradient is kept because it is what gives the face
     * a light direction, and it is already low-alpha (< 0.06) so it survives the fade
     * without becoming a panel.
     *
     * ⚠ ORDER MATTERS: `background-image` must sit AFTER `background` or the
     * shorthand resets the gradient to `none` — which is exactly what the first
     * version of this tier measured (`bgImage: 'none'`).
     *
     * ★★ THE BLACK WASH DESATURATED THE THEME (v9) — IT IS GONE. The third stop used
     * to be a flat `rgba(0, 0, 0, 0.07)`, and while the base sat at 42% alpha that
     * black mixed toward NEUTRAL — which is precisely how burgundy rendered grey and
     * ivory rendered invisible. A neutral wash over a translucent surface can only
     * grey it, so it fought the themed fill it was supposedly decorating. The light
     * direction is carried by the WHITE stops (`0.055` → `0.010`), which read as a
     * soft top-lit face on any hue; the strip's bottom edge is already defined by the
     * `inset 0 -1px 0` shadow below, which is hue-neutral.
     *
     * ══ ★★★ v10 — THE TIER WAS DELETING THE THEME'S METAL, NOT JUST FADING IT ══
     *
     * REPORTED (again, and this time with the diagnostic that cracked it):
     *     "for any question other than the agent related question … if i ask how many
     *      languages, the theme changes to ink and gold for the answer, and when quiet
     *      comes back to burgundy, which i have selected at the backend. but if i ask
     *      for cleopatra and any other agent, it keeps the selected theme."
     *
     * ⚠ THE OBSERVATION WAS RIGHT AND THE EXPLANATION WAS NOT: it is not about AGENTS.
     * It tracks REPLY LENGTH. An agent bio is long → `whisper="0"` → the full slab,
     * gradient and all. "How many languages" is short → `whisper="1"` → THIS tier.
     * Two different questions about the same person would have split the same way.
     *
     * ── WHY v9's FIX WAS ONLY HALF THE STACK ─────────────────────────────────
     * v9 repaired the tier's `background` (the `--eeh-orb-ink` color-mix above) and the
     * measurement confirms that half is still correct — the fill IS the chosen theme's
     * ink in every palette. But the panel is painted from FOUR layers (see the base
     * rule: fill, sheen, metal gradient, bevel), and this declaration used to REPLACE
     * the base `background-image` — which carries the sheen AND the 180deg metallic
     * gradient — with a single hue-neutral white wash.
     *
     * ⚠ `background-image` DOES NOT INHERIT, so "replacing" is the correct word:
     * nothing brought the metal back. The theme's CHARACTER is carried by
     * `--eeh-panel-metal-a/b/c` and by the tinted `--eeh-panel-sheen`; without them the
     * strip is left with nothing but the ink and a white haze — and a flat ink plus
     * white haze looks like the SAME shape of surface in all ten palettes. On Ink &
     * Gold that is invisible, because its ink is already near-black and matches the
     * neutral look; the strip simply appears to "go back to the default", which is
     * exactly what was reported.
     *
     * MEASURED on the real orb, midnight theme, same session, two consecutive turns:
     *     whisper="1"  bg=color(srgb 0.031 0.047 0.102 / 0.65)
     *                  bgImage=linear-gradient(180deg, rgba(255,255,255,0.055) → …)
     *                          ← NO metal stops at all
     *     whisper="0"  bg=rgba(8, 12, 26, 0.88)
     *                  bgImage=…sheen…, linear-gradient(rgba(40,56,104,0.92) →
     *                          rgba(20,28,60,0.92) → rgba(5,8,18,0.96))  ← the hue
     *
     * ⚠ AND BOTH EXISTING LOCKS WERE GREEN ON THAT CODE. `orb_theme_test.py` T10 only
     * ever inspected THIS block in isolation (so it could not see the base gradient
     * being overridden), and `.dev/theme_wash_all.py` sampled `backgroundColor` ONLY —
     * never `background-image`. A lock that reads the property that did not break
     * cannot fail. T10 now asserts the metal vars are present here, and V27 in
     * `orb_visual_test.py` asserts it on the COMPUTED style of the live orb.
     *
     * ── THE FIX: RE-DECLARE THE WHOLE STACK, HELD BACK — DON'T DELETE IT ─────
     * This tier's identity is a DISSOLVED frame, so the metal is deliberately SOFTER
     * than the base rule rather than equal to it. The factor is ~62% of the base stops,
     * which keeps the strip reading as a stripe while the hue stays unmistakable — the
     * same reasoning (and the same band) as the `74%` on the fill above, so the two
     * layers soften TOGETHER and the strip dissolves as one object. Fading only the
     * fill while the material stayed at full strength is what made the earlier
     * attempts read as a colour change rather than as a dissolve.
     *
     * ⚠ THE STOP ORDER AND THE LAYER ORDER ARE COPIED FROM THE BASE RULE ON PURPOSE.
     * Keep the two declarations diffable line-for-line: the only legitimate difference
     * between them is the alpha factor. If you change one, change the other.
     *
     * ⚠⚠ EVERY `var()` KEEPS ITS LITERAL FALLBACK, for the reason the base rule
     * documents: an unresolvable `var()` inside `linear-gradient()` invalidates the
     * WHOLE declaration silently, and the panel would render flat with no console
     * error. The fallbacks below are the DEFAULT (Ink & Gold) stops scaled by the same
     * ~62%, so a page with no theme block degrades to the correct default look.
     *
     * ⚠ THE WHITE WASH STAYS ON TOP. It is what gives the strip a light direction on
     * any hue, it is the only part that was ever hue-neutral by design, and it is why
     * the "no black wash" assertion in T10 must keep passing.
     */
    background-image:
      /* layer 1 — the tinted sheen (the theme's own highlight, held back) */
      linear-gradient(
        100deg,
        var(--eeh-panel-sheen, rgba(255, 255, 255, 0.03)) 0%,
        rgba(255, 255, 255, 0) 38%,
        rgba(255, 255, 255, 0) 62%,
        var(--eeh-panel-sheen, rgba(255, 255, 255, 0.03)) 100%
      ),
      /* layer 2 — ★ THE THEME'S METAL, RESTORED (v10). This is the whole fix: the
         tier must RE-DECLARE the hue rather than let its own declaration delete it.
         The literal fallbacks are the base gradient's Ink & Gold stops at ~62%. */
      linear-gradient(
        180deg,
        var(--eeh-panel-metal-a, rgba(36, 36, 41, 0.57)) 0%,
        var(--eeh-panel-metal-b, rgba(19, 19, 21, 0.56)) 46%,
        var(--eeh-panel-metal-c, rgba(6, 6, 7, 0.58)) 100%
      ),
      /* layer 3 — the white directional wash (unchanged from v9) */
      linear-gradient(
        180deg,
        rgba(255, 255, 255, 0.055) 0%,
        rgba(255, 255, 255, 0.010) 52%,
        rgba(255, 255, 255, 0.030) 100%
      );
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.07),
      inset 0 -1px 0 rgba(0, 0, 0, 0.18),
      0 2px 4px rgba(0, 0, 0, 0.18),
      0 8px 22px rgba(0, 0, 0, 0.20);
    gap: 4px;
    padding: 9px;
    /* ★ THE BLUR CARRIES WHAT THE ALPHA NO LONGER DOES (v9).
     *
     * The wash used to be protected by being almost opaque; now the base keeps its
     * COLOUR (~0.72) instead, so more of the page reaches the strip. That is the
     * point — the dissolve is the requested look — but it puts legibility back on
     * the blur, and a 9px radius was tuned for a 34% fill. Raised to 14px so her
     * words keep their contrast on a busy page while the page still reads as
     * present-but-out-of-focus.
     *
     * ⚠ `-webkit-` IS REQUIRED: iOS Safari only gained unprefixed `backdrop-filter`
     * in 18, and this site's primary device is an iPhone. Without the prefix the
     * whisper tier would be legible on the dev box and unreadable on a phone.
     * ⚠ WHERE `backdrop-filter` IS UNSUPPORTED ENTIRELY the tier merely gets lighter
     * — it does not break, because the falling-back default is a solid `rgb()` only
     * if the alpha is dropped, and this declaration is additive. */
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
  }
  /* Her words lead in this tier: the status row is redundant with the orb's own
     ring motion (the documented MOTION LANGUAGE), and dropping it is what keeps a
     one-line reply from costing a second row. */
  .eeh-orb-panel[data-eeh-orb-whisper="1"] .eeh-orb-state {
    display: none;
  }
  /* ★ TWO LINES, NOT FOUR. This is the promise that a short reply cannot produce
     a tall box, and `_captionLines()` reads this value from the computed style so
     the read-along's rolling window follows it automatically — there is no second
     place to keep in sync. 2 x 20.3px = 41px at line-height 1.45 with 14px text. */
  .eeh-orb-panel[data-eeh-orb-whisper="1"] .eeh-orb-caption {
    /* ★ S-ORB-INSTRUMENT-CAPSULE (2026-10-11) — 41px -> 68px.
     *
     * The brief: "need to be able to fix all the text if too much, like adopting
     * the size to fit the text … the size is flexible, to accomodate the text."
     *
     * The whisper tier fires when the caption MEASURES short (`_syncDock()` reads
     * the rendered height), so this cap is the ceiling for that tier. At 41px it
     * was TWO lines — which meant a reply that genuinely needed three lines was
     * CLIPPED into the whisper tier rather than being allowed to expand, i.e. the
     * hull refused to adopt the text's size. 68px is the same 68px budget the orb
     * and the field already use, so the tier can hold a normal 3-line reply whole
     * without flipping into the full framed slab.
     *
     * ⚠ IT IS STILL A CEILING, NOT A HEIGHT. `_fitCaptionToText()` sizes the box
     * to its own `scrollHeight` first; this is the pre-JS fallback and the cap for
     * a harness with no layout engine. A one-line reply still renders ONE line —
     * the point is only that a three-line one is no longer forced short. */
    --eeh-orb-caption-max: 68px;
  }

  /* ── ★ HER WORDS KEEP OUT OF HER OWN COLUMN (2026-09-22, v2) ──────────────
   *
   * WHY THIS IS LOAD-BEARING, NOT DECORATION. v1 of the dock held the layout
   * clear of her with a 67px `padding-bottom` band across the whole bar — the
   * dead space the visitor reported as "the size of the caption window is too
   * high". Removing it (v2) is only safe if no TEXT can reach the bar's bottom
   * right region, because that region is now where SHE lives.
   *
   * ⚠ IT MUST GO ON THE TEXT ELEMENTS, NOT ON `.eeh-orb-head` (measured, twice).
   * Two failed attempts taught this, and both failures were invisible in a
   * screenshot:
   *
   *   1. Putting it on `.eeh-orb-form` alone left her CAPTION running under her
   *      own portrait — the input was fixed and the text was not.
   *   2. Putting it on `.eeh-orb-head` (the flex parent of both) changed nothing.
   *      `.eeh-orb-head` is `display: flex`, so its padding shrinks its CONTENT
   *      box — but `.eeh-orb-caption` and `.eeh-orb-state` are flex children that
   *      FILL that content box and then add their OWN `padding: 0 12px` on top.
   *      The element measured narrower and the text did not move at all.
   *
   * ⚠⚠ AND IT MUST OUT-SPECIFY THE BASE RULES, HENCE THE `#eeh-orb-panel` PREFIX.
   * The base `.eeh-orb-caption` and `.eeh-orb-state` declare `padding: 0 12px` and
   * sit LATER in this file, so a bare `.eeh-orb-caption { padding-right: … }`
   * loses the cascade and the value silently computes back to 12px — exactly what
   * happened on the first attempts. A single ID prefix makes the intent explicit
   * and cannot be overturned by a later single-class rule. Do not "simplify" it.
   *
   * ★ THE CLEARANCE IS THE CAPPED DIAMETER, NOT THE BAR HEIGHT. This is the
   * second half of the feedback fix (see `--eeh-orb-max` on the orb). Using
   * `--eeh-orb-height` here meant a longer reply → taller bar → wider clearance →
   * narrower caption → taller wrap → wider clearance, and the probe caught the
   * two chasing each other with her left edge at 1307.1 against text ending at
   * 1328. Reading the SAME cap the orb reads ends the loop: the space reserved
   * and the space she occupies are now literally the same expression, so they
   * cannot disagree. */
  #eeh-orb-panel .eeh-orb-caption,
  #eeh-orb-panel .eeh-orb-state,
  #eeh-orb-panel .eeh-orb-form {
    --eeh-orb-max: 93px;
    padding-right: calc(12px
                        + clamp(var(--eeh-orb-size),
                                var(--eeh-orb-height, var(--eeh-orb-size)),
                                var(--eeh-orb-max))
                        + 14px);
  }
  /* The chips are an ANSWER surface, not a text flow: they sit above the input
     and never reach her column, so they keep the full width. */
}

/* ── ★ THE CLEAR ZONE ENDS WHERE THE PHONE STAGE BEGINS ────────────────────
 * On a phone the panel is a full-screen stage and the orb is a centred hero
 * ABOVE the text — her column overlaps nothing, so reserving text width would
 * waste a thumb's worth of the screen for no reason. The two languages must
 * agree about the breakpoint, so this is written against the same 480px the
 * stage and `_isPhoneWidth()` use, and `V1 the_desktop_dock_does_not_leak_onto_
 * the_stage` measures it on a real 390px viewport. */
@media (max-width: 480px) {
  /* ⚠ These must be as SPECIFIC as the desktop dock rule above (which is
     `#eeh-orb-panel .eeh-orb-caption` etc.), or the dock's clearance would leak
     onto the phone stage and squeeze her text for an orb that is not beside it
     there. Same selectors, same prefixes. */
  #eeh-orb-panel .eeh-orb-form { padding-right: 0; }
  /* The caption and state keep their own quiet text inset on the stage, where her
     column overlaps nothing at all. */
  #eeh-orb-panel .eeh-orb-caption,
  #eeh-orb-panel .eeh-orb-state { padding-right: 12px; }
  /* ★ THE FOLD CONTROL'S DESKTOP MARGIN IS GONE WITH THE CONTROL ITSELF
     (S-ORB-NO-MINUS, 2026-10-10). This rule used to reset `.eeh-orb-min`'s
     margin on the phone stage; with the button deleted there is nothing to
     reset, so the rule is removed rather than left matching nothing.
     ⚠ The `::after` 44px-target note it carried is also gone — that pseudo-
     element belonged to `.eeh-orb-min` only. */
}

/* ── the mobile stage header (hidden on desktop) ─────────────────────────
 * On a phone the panel becomes FULL SCREEN (see the ≤480px block) and this is
 * the strip across its top: a grip that says "this sheet can go away", and the
 * − that does it. Both are inert above 480px, where the panel is a bar beside
 * the orb and `_collapse()` is unnecessary (the bar is already out of the way).
 */
.eeh-orb-mobile-bar { display: none; }

/* ── her text: the read-along line + the status row ─────────────────────── */

.eeh-orb-head {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

/* ── ★ THE HEAD'S TOP ROW: HER WORDS AND THE FOLD CONTROL (v6b) ────────────
 *
 * Adding the fold control to the head needed a row, not a rewrite. The head is a
 * column of [caption, status]; the row groups the caption with the control so the
 * control can sit at the TOP-RIGHT while the status stays full-width underneath.
 *
 * ⚠ `align-items: flex-start` IS LOAD-BEARING. With the default `stretch` the
 * control would be stretched to the full height of a multi-line reply, so a
 * four-line answer would produce a 60px-tall ellipsis of a button. It is pinned to
 * the top edge instead — which is also what "top-right corner" means.
 *
 * ⚠ `min-width: 0` on the caption is what lets a long word WRAP instead of pushing
 * the control out of the bar. A flex item's default `min-width: auto` refuses to
 * shrink below its content, so without it a long unbroken string would shove the
 * button past the panel's edge. */
.eeh-orb-headrow {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  min-width: 0;
  /* ★ THE FOLD CONTROL'S KEEP-OUT ZONE, PUBLISHED AS A VARIABLE (v6b).
   *
   * This is the SAME expression the caption's `padding-right` uses (12px + the orb's
   * height capped at `--eeh-orb-max` + 14px), because the two must agree: the caption
   * stops where her column begins, and the fold control must stop in the same place
   * or it lands on her portrait.
   *
   * ⚠ IT LIVES ON THE HEADROW, WHICH IS WHY IT READS `--eeh-orb-max` WITH A FALLBACK
   * RATHER THAN ASSUMING IT EXISTS HERE. `--eeh-orb-max` is published on the ORB and
   * read by the caption via a `#eeh-orb-panel`-scoped rule; the headrow is a different
   * subtree, so the fallback (`68px`, the documented cap) is what actually resolves on
   * the desktop bar. That is deliberate rather than sloppy: the value is a CAP, and a
   * cap that silently becomes `0` when a variable is missing would collapse the
   * control onto her portrait — the exact failure this margin exists to prevent.
   *
   * ⚠ ON THE PHONE STAGE THIS CLEARING DOES NOT APPLY — the orb is a centred hero above
   * the panel, so there is no column to keep out of. The ≤480px block resets it. */
  --eeh-orb-clear: calc(12px
                      + clamp(var(--eeh-orb-size),
                              var(--eeh-orb-height, var(--eeh-orb-size)),
                              var(--eeh-orb-max, 93px))
                      + 14px);
}
.eeh-orb-headrow .eeh-orb-caption { flex: 1 1 auto; min-width: 0; }

/* ── ★ THE FOLD CONTROL'S TOUCH TARGET — SHARED BY BOTH SURFACES (v6b) ──────
 *
 * ⚠ THIS RULE MUST LIVE **OUTSIDE** THE DESKTOP MEDIA BLOCK, AND THAT IS A BUG FIX.
 * The first version put it inside `@media (min-width: 481px)` alongside the desktop
 * styling, so on a 390px phone the pseudo measured `content: 'none'` — i.e. the
 * button had NO hit area at all beyond its own 26px chevron, on the one platform
 * where the 44px minimum actually matters. Only mobile QA could see it.
 *
 * ★ THE TARGET IS A SEPARATE CONCERN FROM THE POSITION. Where the control sits is
 * a per-surface decision (the head on desktop, the stage header on a phone); how
 * big a thumb target it offers is not. Keeping them in the same media query coupled
 * two unrelated things and silently dropped one of them.
 *
 * ⚠ CENTRED AND FIXED AT 44px, NOT INSET BY A MARGIN. An `inset: -9px` form is
 * correct only while the button is 26px; the phone stage overrides it to 40px, so
 * `-9px` would produce a non-square target. Sizing it absolutely and centring it
 * makes the target exactly 44px whatever the painted button measures. */
/* ── ★ THE FOLD CONTROL — REMOVED (S-ORB-NO-MINUS, 2026-10-10) ────────────────
 *
 * ── WHAT WAS HERE, AND WHY IT IS GONE ───────────────────────────────────────
 * A `.eeh-orb-min` button: a 26px circle (44px painted on the phone stage)
 * holding a 16px `−` glyph, plus its `::after` 44px touch target. Its whole job
 * was to call `_collapse()`.
 *
 * ── THE VISITOR'S REPORT (and it is right) ──────────────────────────────────
 *   "the minus - sign minimize the caption windows, to the orb, how about
 *    removing this, keep the speaker icon only, that gives more lenght to the
 *    text box also, will look balanced, and the reducing or shutting her off
 *    funciton can be achived, by clicking the orb circle"
 *
 * ★ TWO CONTROLS, ONE VERB. The `−` duplicated an action the orb can perform
 * itself. Removing it hands the whole row width to the field — the "more length
 * / look balanced" half of the request — and the row settles at
 * `[ 🔊 speaker ] [ ————— field ————— ]` (speaker LEFT, per the visitor's
 * follow-up choice).
 *
 * ⚠ THE HISTORY IS KEPT RATHER THAN ERASED, BECAUSE THE SAME VISITOR ONCE ASKED
 * FOR THIS CONTROL. v7's note here read: "the visitor asked for exactly this ('a
 * minus sign next to the speaker icon is good, that will minimize')". That was a
 * real request and it was honoured for several releases. This pass REVERSES it,
 * which is why the reversal is written down — the same discipline the mute's
 * always-visible note uses (two verdicts, both recorded, neither silently
 * overwritten).
 *
 * ★ THE ACTION DID NOT DISAPPEAR; IT MOVED TO THE ORB. `_bind()` now TOGGLES on
 * the orb's click and keydown (see `eeh-orb-loop.js`), so `_collapse()` is still
 * reachable with every effect intact: `_peek(true)` restores the "is here" pill,
 * `_releaseTour()` returns the tour button LAST (S-TOUR-ORB-PRIORITY), and the
 * drag/tap click-swallow still guards it. Deleting the button WITHOUT that toggle
 * would have stranded the visitor with no way to close her — which is why the
 * two changes ship together and the gate asserts both.
 *
 * ⚠ THE `.eeh-orb-min` SELECTOR IS NOW MATCHED BY NO MARKUP. It is deleted rather
 * than left dead: a rule that can never match tells the next reader the control
 * exists, and it does not (the same defect the FAB's dead `--playing` rule was
 * removed for). Verified by search: the only consumers were the JS that built it
 * and the gate assertion that named it, both updated in this pass.
 */

/* ★ NO DEAD SPACE (the "unbalanced" half of the report). When she has said
   nothing AND is not mid-turn, the whole head disappears and the panel is a
   single slim input bar. `:empty` covers both children, and because both are
   built empty and only ever filled by JS, this is exact rather than heuristic. */
.eeh-orb-head:empty { display: none; }

/* The status line: four short warm strings ("I'm listening…"). Deliberately
   quiet — 12px, 62% white, indented by 12px so it aligns with the INPUT's text
   rather than the bar's edge. It is a whisper about the microphone; her actual
   words get the size and the contrast below. */
.eeh-orb-state {
  font-size: 12px;
  line-height: 1.3;
  color: rgba(255, 255, 255, 0.62);
  padding: 0 12px;
  letter-spacing: 0.01em;
}

.eeh-orb-state:empty { display: none; }

/* ── the input row ─────────────────────────────────────────────────────── */

.eeh-orb-row {
  display: flex;
  align-items: center;
  /* ★ S-ORB-FIELD-SLIM (2026-10-11) — 2px -> 0. The mute's 30px disc is the
     field's left boundary now, so the row reads as ONE control (a disc and a
     groove) rather than as three objects in a line. The 2px this frees goes
     straight to the field, which is the entire point of this pass. */
  gap: 0;
  min-width: 0;
}

/* Show exactly one glyph depending on the state. */
.eeh-orb-mute .eeh-orb-mute-off { display: none; }
.eeh-orb-mute.eeh-orb-muted .eeh-orb-mute-on  { display: none; }
.eeh-orb-mute.eeh-orb-muted .eeh-orb-mute-off { display: inline; }

/* ★ MUTE — ALWAYS VISIBLE NOW (v7), AND THAT REVERSES v6 ON PURPOSE.
 *
 * The visitor's report, and it is precise:
 *
 *     "speaker icon is not visible but only when scrolled upon, should be visible
 *      all the time"
 *
 * ⚠ THE OLD BEHAVIOUR WAS DELIBERATE, NOT A BUG, and the comment it replaced said
 * so: it shipped at `opacity: 0` and appeared on hover / focus / once engaged,
 * because an earlier visitor had reported a permanently visible speaker as
 * "on top speaker icon to mute … a large part of why the panel felt unbalanced".
 * Two visitors, two opposite verdicts, which is exactly why this is written down
 * rather than silently flipped.
 *
 * ★ WHAT MAKES THE NEW VERDICT RIGHT: the panel is now much smaller than when the
 * original complaint was made. The unbalanced panel was the 157px slab with a
 * status row, a caption and a wide input; today it is a 66px strip, and the mute
 * has moved from floating ABOVE the caption into the input row where it shares a
 * baseline with the field. The thing that made it feel like chrome is gone, so
 * hiding it now costs more than it saves — a control the visitor cannot see is one
 * they cannot use, and the two moments it is most wanted (she is loud in a shared
 * room; she will not stop talking) are both moments when hovering is the LAST thing
 * on their mind.
 *
 * ⚠ SO: NO `opacity: 0`, AND THE THREE REVEAL RULES ARE DELETED. `OB-29` (mute
 * exists, silences audio, remembers the choice) and `OB-30` (a muted visitor still
 * READS every reply in the caption) are untouched — this changes only WHEN the
 * control is painted, never what it does. The "silence her voice, keep her words"
 * accessibility need is intact and now easier to reach.
 *
 * ⚠ KEEPING `opacity` AS THE MECHANISM, NOT `display` — the rules below that hide
 * one GLYPH or the other depend on the button box existing at all times.
 */
.eeh-orb-mute {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: rgba(255, 255, 255, 0.72);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
  /* ★ NO `opacity: 0` (v7). The reveal rules that used to follow are DELETED, not
     disabled — see the block comment above for why the visitor's verdict reversed
     the original one. */
  transition: background 0.15s ease, color 0.15s ease;
}

.eeh-orb-mute:hover,
.eeh-orb-mute:focus-visible {
  background: rgba(255, 255, 255, 0.10);
  color: #fff;
  outline: none;
}

/* Show exactly one glyph depending on the state. */
.eeh-orb-mute .eeh-orb-mute-off { display: none; }
.eeh-orb-mute.eeh-orb-muted .eeh-orb-mute-on  { display: none; }
.eeh-orb-mute.eeh-orb-muted .eeh-orb-mute-off { display: inline; }

/* A muted control must stay VISIBLE even while she is silent, or the visitor has
   no way to discover how to turn her voice back on. This is the one case where
   the control outranks the "only when relevant" rule — an engaged state the
   visitor cannot undo is worse than a visible button. */
.eeh-orb-panel .eeh-orb-mute.eeh-orb-muted {
  opacity: 1;
  color: var(--eeh-orb-gold-lit);
}

/* The caption states what is happening, in words, at all times. On a touch
   device this is the ONLY textual signal (the hover-pill never shows).

   ★ THIS IS THE READ-ALONG LINE (2026-09-20: it now holds ONLY her words).
   The four status strings ("I'm listening…") moved to `.eeh-orb-state` above, so
   anything painted here is her actual reply — which means the box may size itself
   to its text instead of reserving 80px for four words.

   ── WHAT IS PRESERVED FROM THE 2026-09-16 FIX ─────────────────────────────
   The trailing window + the bottom scroll pin are UNCHANGED and both live in
   `_showWindow()` / `_paintCaption()`. They solve a real, reported bug: growing
   prefixes plus `overflow: hidden` clipped the NEWEST words (exactly the ones she
   was saying). JS still renders the tail and pins `scrollTop = scrollHeight`, so
   the visible lines are always the newest ones — subtitle behaviour.

   ★ WHAT CHANGED, AND WHY IT IS SAFE. The old rule was a hard
   `height: 80px; min-height: 80px; max-height: 80px`. `min-height: 80px` is what
   forced the dead space; removing it and capping with `--eeh-orb-caption-max`
   (which `_captionLines()` reads as 4 lines, and `_showWindow()` uses to size its
   window) gives one strict improvement: a reply that needs at most 4 lines — the
   overwhelming majority — LOOKS EXACTLY AS IT DOES TODAY, pixel for pixel, with
   the same bottom pinning and the same "newest word is visible" guarantee. A
   longer reply clips at 4 lines instead of 3, which is strictly MORE text, and
   the newest words are still the ones shown.

   `_captionLines()` reads the computed style, so the JS follows this variable
   automatically — there is no second place to update. */
.eeh-orb-caption {
  /* ★ THE CAPACITY, as one variable: 4 lines at line-height 1.45 with 14px text
     (4 × 20.3 = 81.2px). `overflow: hidden` clips at this point and JS pins the
     scroll, so it behaves as a "show the newest 4 lines" cap rather than a
     growing box. Measured from the same numbers `_captionLines()` derives. */
  --eeh-orb-caption-max: 82px;
  padding: 0 12px;
  /* ★ THEMED (v8) — her words read from the palette so a light theme (Ivory)
     can use dark text. Fallback is the shipped white, so an absent theme is unchanged. */
  color: var(--eeh-orb-text, #fff);
  line-height: 1.45;
  /* Her read-along line is the loudest text in the bar: full white at 14px while
     the status whisper above it is 12px at 62%. That contrast IS the hierarchy. */
  font-size: 14px;
  /* ★ AUTO HEIGHT (was a fixed 80px — the source of the dead space). The box is
     exactly as tall as her words up to the cap; the bottom pin keeps the newest
     line in view; and `overflow: hidden` means no scrollbar can ever appear. */
  max-height: var(--eeh-orb-caption-max);
  overflow: hidden;
  /* Long unbroken strings (URLs, codes) must wrap, never force a wider box. */
  overflow-wrap: anywhere;
  word-break: break-word;
}


/* ======================================================================
 * TIER 3 - THE SETTLED CAPTION (2026-10-01)
 *
 * Her reply stops being a SUBTITLE and becomes a PARAGRAPH. Set by
 * `_settleCaption()` the moment her voice ends, and cleared by
 * `_clearSettle()` on the next turn.
 *
 * WHY IT EXISTS. The caption is `overflow: hidden` with `scrollTop` pinned to
 * the bottom, so while she speaks only the NEWEST lines are visible. That is
 * correct for live subtitles and WRONG the moment she stops: the bottom pin
 * then shows the tail of a 4-line window and a long reply looks truncated at
 * the top. Nothing is deleted - the earlier words are pushed out of view.
 *
 * THE CEILING IS DERIVED, NOT COPIED FROM CT.
 *   CT uses `min(340px, 42vh)`. EEH already caps the caption at 40vh in its own
 *   responsive tiers below, so the settled cap must not exceed what EEH already
 *   applies at that breakpoint. Hence `min(320px, 38vh)`.
 *
 * IT IS A CEILING, NOT THE SIZE. `_fitCaptionToText()` asks the TEXT
 * (`scrollHeight`) and sets `--eeh-orb-settled-max` per reply; this rule is the
 * pre-JS fallback and the cap when a harness has no layout engine. CT measured
 * three fixed values too small (82 -> 102 -> 124px), each against a shorter
 * reply than the next - a fixed number must be re-guessed every time.
 *
 * SCOPED TO THE SETTLED ATTRIBUTE AND NOTHING ELSE.
 *   CT records that a blanket wide cap broke V18, V28 AND V29 at once:
 *   "Not settled" is not the same claim as "not speaking", and
 *   "a tier is not a state." The whisper tier (41px, 2 lines) and the base
 *   reading tier (82px, 4 lines) keep their own caps UNDISTURBED.
 *
 * ⚠ NEVER key a caption cap on `:not([data-eeh-orb-settled="1"])` - that
 *   selector is TRUE WHILE SHE IS SPEAKING, so it lands on the read-along.
 *   `.dev/verify-caption-tiers.php` fails the build if that shape appears.
 * ====================================================================== */
.eeh-orb-panel[data-eeh-orb-settled="1"] .eeh-orb-caption {
  /* The ceiling for a genuine essay. A normal reply fits WHOLE because the JS
     sizes the box to its own scrollHeight first. */
  --eeh-orb-settled-limit: min(420px, 46vh);
  /* Per-reply override when the JS has measured, else the ceiling. */
  --eeh-orb-caption-max: var(--eeh-orb-settled-max, var(--eeh-orb-settled-limit));
}

.eeh-orb-caption:empty { display: none; }

/* The type-instead-of-speak row. */
.eeh-orb-form {
  display: flex;
  gap: 4px;
  align-items: center;
}

/* ══════════════════════════════════════════════════════════════════════════════
 * ★★ S-ORB-INSTRUMENT-CAPSULE (2026-10-11) — A TRANSPARENT GOLD CAPSULE, AND
 *    THIS SELECTOR IS WHY IT FINALLY RENDERS.
 *
 * ── ★★★ THE BUG THREE PASSES COULD NOT SEE ─────────────────────────────────
 * Rounds 1 (S-ORB-INPUT-BALANCE), 2 (S-ORB-FIELD-MATERIAL) and 3
 * (S-ORB-FIELD-SLIM) each edited the rule below CORRECTLY, each passed every
 * static gate, and each was a SILENT NO-OP ON THE RENDERED PAGE.
 *
 * The Neve theme ships, in `style-main-new.min.css`:
 *
 *     form input:read-write {
 *       background: var(--formfieldbgcolor);   // -> var(--nv-site-bg) -> #ffffff
 *       color: var(--formfieldcolor);
 *       border-color: var(--formfieldbordercolor);
 *       border-width: var(--formfieldborderwidth);   // -> 2px
 *       padding: var(--formfieldpadding);            // -> 10px 12px
 *       border-radius: var(--formfieldborderradius, 3px);
 *       line-height: var(--formfieldlineheight);
 *       font-family: var(--bodyfontfamily);
 *       font-size: var(--formfieldfontsize);
 *       text-transform: var(--formfieldtexttransform);
 *       letter-spacing: var(--formfieldletterspacing);
 *     }
 *
 * ⚠ `form input:read-write` is (0,1,3). A bare `.eeh-orb-input` is (0,1,0).
 *   THE THEME WINS EVERY CONFLICTING PROPERTY. Measured on the live page
 *   (`.dev/probe-orb-field-rendered.py`, BEFORE this fix):
 *
 *       background     rgb(255, 255, 255)     <- the "large bright white box"
 *       border         2px solid rgb(221,221,221)
 *       padding        10px 12px              <- the "too thick"
 *       height         47.8px
 *       border-radius  3px
 *
 *   NOTHING IN THE PLUGIN COULD SEE IT. Every static gate asserts on FILE
 *   CONTENTS; the fault was in the CASCADE, against a stylesheet that lives in
 *   the THEME. `.clinerules` §5.4 says it exactly: "assert on the winning
 *   cascade, not on file contents." Three rounds were spent rediscovering this.
 *
 * ── ★ THE FIX, IN TWO PARTS, AND BOTH ARE LOAD-BEARING ─────────────────────
 *   1. `#eeh-orb-panel .eeh-orb-form .eeh-orb-input` is (1,3,0) — an ID seals
 *      the specificity question permanently. The `:read-write` twin is listed
 *      so the rule also matches the pseudo-class LEVEL the theme uses.
 *   2. EVERY property the theme declares is RE-CLAIMED BELOW. Winning on
 *      specificity while leaving one property undeclared simply hands that one
 *      property back — which is how a "fixed" field can still arrive wrong.
 *
 * ── ★ THE MATERIAL: TRANSPARENT + A GOLD HAIRLINE (the operator's brief) ───
 * "the text box can be transparent, with a thin gold line around it, like a
 *  capsule"
 *
 * This is not only the requested look, it is STRUCTURALLY IMMUNE to the bug
 * above: a field with NO FILL cannot be repainted bright by any theme variable,
 * present or future. The earlier fills kept losing this fight in a new colour
 * (`rgba(255,255,255,0.06)` -> `rgba(0,0,0,0.28)` -> `rgba(120,116,128,0.22)`);
 * removing the fill removes the battlefield.
 *
 * ⚠ THE GOLD CREPT IN AS A BORDER, NOT A GLOW. `--eeh-orb-gold` is the palette's
 * accent token (the orb ring already uses it), so the capsule reads as part of
 * the same instrument rather than as a new colour. A hairline, not a halo: at
 * 1px it defines the capsule's edge without becoming the brightest thing on the
 * bar — which is exactly how the white box earned its complaint.
 *
 * ⚠ `box-shadow: none` IS DELIBERATE, NOT AN OMISSION. The inset two-pass bevel
 * that rounds 1-3 built (a dark top lip + a light bottom lip) is a RECESS cue,
 * and a recess needs a surface to be sunk into. On a transparent field the only
 * thing those shadows would shade is the hull behind it, smudging the capsule's
 * own hairline. Depth now comes from the hull's bevel instead (see the panel
 * rule) — one component owns the depth, not two competing.
 * ══════════════════════════════════════════════════════════════════════════════ */
#eeh-orb-panel .eeh-orb-form .eeh-orb-input,
#eeh-orb-panel .eeh-orb-form .eeh-orb-input:read-write {
  flex: 1 1 auto;
  min-width: 0;
  /* ── 1. THE FILL: NONE. This is the fix AND the brief — see the block above. */
  background: transparent;
  background-image: none;
  /* ── 2. THE CAPSULE: a 1px gold hairline, both ends fully round. */
  border: 1px solid var(--eeh-orb-gold, #CD853F);
  border-radius: 999px;
  /* ── 3. SLIM. 3px + 3px + 2px border + a 1.12 line box = ~24px on 14px text.
     ⚠ 3px IS THE FLOOR: below it the descender of a single-line 14px input
     starts to crowd the hairline at the glyph's own box. */
  padding: 3px 14px;
  line-height: 1.12;
  /* ── 4. THE TEXT — the dedicated composer token. `--eeh-orb-text` is
     deliberately NOT the fallback: on the live palette it resolves to `#fff4f6`
     (near-pure white), so chaining to it would make the soft literal unreachable
     and the softening a no-op. That trap has bitten this file more than once. */
  color: var(--eeh-orb-field-text, #e9e2e4);
  /* ── 5. THE THEME'S TYPOGRAPHY, RE-CLAIMED. These matter more than they look:
     a Neve Customizer value in `--formfieldtexttransform` would silently
     UPPERCASE everything the visitor types, and `--formfieldletterspacing`
     would stretch it. Neither is hypothetical — both are LIVE variables. */
  font-family: inherit;
  font-size: 14px;
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
  /* ── 6. THE NATIVE WIDGET OFF. Without this the UA's own input chrome can
     reappear on some engines regardless of the background declaration. */
  -webkit-appearance: none;
  appearance: none;
  outline: none;
  /* ── 7. NO INSET BEVEL — the hull owns the depth now; a recess cue on a
     transparent field would only smudge its own hairline. */
  box-shadow: none;
  /* ⚠ KEEP THE FILL INSIDE THE HAIRLINE. With a transparent field the theme's
     own background can otherwise paint into the border's rounded corners and
     leave a hairline of the wrong colour at the capsule's ends. */
  -webkit-background-clip: padding-box;
  background-clip: padding-box;
  /* ★ THE CAPSULE REACHES TOWARD THE ORB (kept from S-ORB-FIELD-SLIM).
     `#eeh-orb-panel .eeh-orb-form` reserves `12px + clamp(56px..68px) + 14px`
     (~94px) so her text never slides under the portrait. A slice of that
     reserve is spent here so the capsule runs visibly closer to the orb.
     ⚠ ARITHMETIC, NOT HOPE: the reserve is ~94px and the orb is capped at 68px,
     so -12px leaves ~14px of clearance still standing between the capsule's
     right end and her left edge. If `--eeh-orb-max` is ever raised, RE-DERIVE
     this number — it is a slice of a reserve, so it scales with the thing it
     slices. */
  margin-right: -12px;
  /* ★ KEPT FROM v7 — the hit-test stacking. The capsule must win with
     `z-index: 1` against the mute's `0`, or 44px of padding-free button box
     adjacent to it eats taps at its right edge. */
  position: relative;
  z-index: 1;
}

/* ★ THE FOCUS RING — ID-SCOPED FOR THE SAME REASON AS THE RULE ABOVE.
   The hairline brightens and gains a soft gold halo, so "the capsule is live"
   reads at a glance WITHOUT the field ever gaining a fill — which is the thing
   this whole pass removed. */
#eeh-orb-panel .eeh-orb-form .eeh-orb-input:focus,
#eeh-orb-panel .eeh-orb-form .eeh-orb-input:focus-visible {
  background: transparent;
  border-color: var(--eeh-orb-gold, #CD853F);
  box-shadow: 0 0 0 3px rgba(205, 133, 63, 0.16);
  outline: none;
}

/* ══ ★ THE PLACEHOLDER — ID-SCOPED (2026-10-11, S-ORB-INSTRUMENT-CAPSULE) ═════
 *
 * ⚠ IT MUST BE SCOPED FOR THE SAME REASON THE FIELD ITSELF IS. The theme sets
 * `color` on `input:read-write`, and a `::placeholder` pseudo-element INHERITS
 * that colour — so an unscoped hint can be dragged to the theme's colour along
 * with the fill. Scoping keeps it on the palette's `--eeh-orb-dim` decision.
 *
 * ── WHY `--eeh-orb-dim` AND NOT A LITERAL (kept from v10, still true) ───────
 * It was the flat literal `rgba(255, 255, 255, 0.55)`: a WHITE placeholder,
 * hard-coded. On the nine dark palettes that is correct by coincidence. On a
 * palette whose `ink` is near-white and whose `text` is near-black it rendered
 * WHITE ON A NEAR-WHITE FIELD — invisible, and nothing hinted at it.
 *
 * ★ AND IT IS WHY `--eeh-orb-dim` EXISTED BUT WAS NEVER USED. The variable was
 * declared in `:root` (and emitted by `orb_theme_css()`) from the first port,
 * and no rule consumed it — so `verify-orb-theme.php` reported it as a dead
 * token with nothing to paint. This is the rule it was always meant for: the
 * VISITOR-facing hint. Removing the token would have been the wrong fix; the
 * missing consumer was.
 *
 * ⚠ THE LITERAL FALLBACK IS 0.62, RAISED FROM 0.55, so a page that has the
 * stylesheet but NOT the PHP-emitted `:root` block — a cached page, a DB reset,
 * a PHP error, the exact cases the fallback exists for — still gets a readable
 * hint.
 *
 * ⚠ NOW THAT THE FIELD IS TRANSPARENT, THE HINT SITS ON THE HULL'S OWN METAL
 * rather than on a field fill. That IMPROVES it: `--eeh-orb-dim` is a
 * per-palette contrast decision calibrated for exactly that dark surface, so
 * the hint is now read on the ground it was always meant for. No re-tune — and
 * a second alpha here would be a second answer to a question the palette
 * already answers.
 * ═════════════════════════════════════════════════════════════════════════════ */
#eeh-orb-panel .eeh-orb-form .eeh-orb-input::placeholder,
#eeh-orb-panel .eeh-orb-form .eeh-orb-input:read-write::placeholder {
  color: var(--eeh-orb-dim, rgba(255, 255, 255, 0.62));
  opacity: 1;                 /* Firefox dims placeholders by default */
}


/* ── ★ THE SEND BUTTON — DELETED (v7) ───────────────────────────────────────
 *
 * The visitor's report: "tell me about the inverted arrow, on the caption windows,
 * what it is, should be removed if not needed". It was the `→` SEND glyph, and the
 * verdict to remove it is right — but for a better reason than "not needed".
 *
 * ── WHAT IT ACTUALLY WAS ──────────────────────────────────────────────────
 * A 30px transparent square holding a 16px gold `<path d="M4 12h13M12 6l6 6-6 6">`:
 * a right arrow that was NOT inverted and NOT broken. It simply READ wrong, because
 * a `→` at the right edge of a field that says "Type or talk…" means either
 * "forward" or "collapse this" to a visitor; "send" is the one reading it does not
 * suggest, and it was repeating a promise the placeholder already makes.
 *
 * ── ★ THE HISTORY MATTERS, BECAUSE THIS WAS DELIBERATELY KEPT TWICE ────────
 *   1. 2026-09-15: "SEND BUTTON — SHRUNK, NOT REMOVED." Kept for MOBILE, where soft
 *      keyboards differ, and as the target screen readers navigate to.
 *   2. It then cost a real bug: its `::after { inset: -7px }` tap target extended
 *      LEFT over the input and painted above it, so `elementFromPoint(inputCentre)`
 *      returned `.eeh-orb-send` and **the text field could not be typed into at all**.
 *      The fix was to grow it with padding instead — i.e. the control was made to
 *      work by being pushed away from the thing it was covering.
 * ★ THAT SECOND POINT IS WHY REMOVING IT IS THE RIGHT CALL RATHER THAN A TIDY-UP. A
 * control that has to be positioned carefully so it does not eat its neighbour's
 * hit area is a control that does not belong in that row — the same lesson as the
 * fold control in v6/v6b.
 *
 * ── ⚠ WHAT REPLACES ITS FUNCTION, AND WHY NOTHING IS LOST ──────────────────
 *   • ENTER STILL SUBMITS THE FORM. `<form>` submits natively on Enter with or
 *     without a submit button (verified: no JS references `.eeh-orb-send`, and
 *     `_onComposerChanged`/the form's own `submit` handler carry the turn), so
 *     desktop keyboard visitors lose nothing.
 *   • THE ROW IS NOW HONEST ABOUT ITS PURPOSE. It is [field, speaker, minus]: type
 *     here · silence her voice · fold her away. Three controls, each adjacent to
 *     what it acts on.
 *   • ⚠ IF A VISIBLE SEND AFFORDANCE IS EVER NEEDED AGAIN, ADD IT AS AN ICON INSIDE
 *     THE FIELD (a trailing button) — NOT as a fourth flex item, which is the shape
 *     that produced both the overlap bug and the crowding that pushed the fold
 *     control into the corner in v6b.
 *
 * ⚠ ALL `.eeh-orb-send` RULES ARE REMOVED, INCLUDING ITS `z-index` LINE IN THE HIT-TEST
 * BLOCK BELOW. Leaving them would be dead CSS that reads as a live intent.
 */
.eeh-orb-mute  { position: relative; z-index: 0; }
/* ★ THE FORM IS A POSITIONING CONTEXT, AND THE CAPSULE MUST WIN THE HIT TEST.
 * ⚠ `position: relative; z-index: 1` ON THE CAPSULE NOW LIVES IN ITS OWN RULE
 * (S-ORB-INSTRUMENT-CAPSULE), NOT HERE. It used to be a second, BARE
 * `.eeh-orb-input { position: relative; z-index: 1; }` declaration down at this
 * point — and a bare class is exactly the shape that lost to the theme. Leaving
 * it would have been harmless-BY-LUCK (the theme does not set `position`) but it
 * would also have re-taught the next reader the wrong pattern in the one file
 * where that pattern cost three passes. One declaration, in the scoped rule.
 * ⚠ WHY IT MATTERS AT ALL: the mute's 44px padding-free box sits adjacent to the
 * capsule, and without `z-index: 1` the capsule loses taps at its right edge. */
.eeh-orb-form { position: relative; }

/* ══════════════════════════════════════════════════════════════════════════
 * ★ MOBILE: THE FULL-SCREEN STAGE (2026-09-20 visual rebuild)
 *
 * ── WHY NOT THE SAME BAR ON A PHONE ───────────────────────────────────────
 * A 380px bar is 97% of a 390px viewport, so the "bar" is already the full width
 * — and it still offers only four lines of her reply while a thumb covers the
 * bottom third of the screen. The visitor's own instinct was right: "perhaps
 * become full screen for mobile when click the orb and minimize - sign on the
 * corner to reduce to orb again."
 *
 * ── THE SHAPE ─────────────────────────────────────────────────────────────
 *   • The panel becomes the WHOLE viewport, minus the safe areas.
 *   • The orb GROWS TO A HERO (112px) and rises into the stage's upper third. It
 *     stays visible and tappable: the conversation never loses her face, which is
 *     the entire reason this product is an orb and not a chat box.
 *   • Her words become the large text of the screen (17px) — the read-along is
 *     what a phone visitor actually reads.
 *   • The input is pinned to the BOTTOM so it lands just above the soft keyboard.
 *   • The header strip (grip + −) is how the visitor returns to the small
 *     floating orb without reloading a page. `_collapse()` in the loop owns that.
 *
 * ★ WHY `.eeh-orb-staged` AND NOT JUST A BREAKPOINT: CSS alone cannot know that
 * the visitor minimised her, and a panel that keeps re-showing itself after a
 * minimise tap is how a − button silently stops working. The class is the single
 * JS-visible fact "the stage is open", and the media query only says "a stage is
 * possible here".
 * ══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 480px) {
  /* ── the orb as the hero of the stage ── */
  #eeh-avp-orb.eeh-orb-staged {
    width: 112px;
    height: 112px;
    /* Centred horizontally, sitting in the upper third: below the status bar,
       well clear of the keyboard, and not crowding the text beneath it. */
    right: 50%;
    bottom: auto;
    top: max(15vh, calc(env(safe-area-inset-top, 0px) + 64px));
    transform: translateX(50%);
    box-shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
  }
  /* The hover/focus scale must compose with the centring transform, not replace
     it — otherwise the orb jumps half its own width on hover. */
  #eeh-avp-orb.eeh-orb-staged:hover,
  #eeh-avp-orb.eeh-orb-staged:focus-visible {
    transform: translateX(50%) scale(1.04);
  }
  #eeh-avp-orb.eeh-orb-staged .eeh-avp-icon { font-size: 44px; }
  /* A 112px hero with the desktop's 6px ring inset would leave the ring looking
     detached; scale the inset with the orb. */
  #eeh-avp-orb.eeh-orb-staged .eeh-avp-ring { inset: -10px; border-width: 3px; }

  /* ── the stage itself ── */
  .eeh-orb-panel {
    inset: 0;
    width: auto;
    max-width: none;
    border: none;
    border-radius: 0;
    gap: 10px;
    /* `padding-top` clears the hero orb: its offset + its height + breathing
       room. `max()` keeps the safe-area floor on notched devices.
       ★ THIS SHORTHAND IS ALSO WHAT UNDOES THE DESKTOP DOCK. The docked panel
       carries `padding-bottom: calc(var(--eeh-orb-size) + 11px)` (to clear the
       orb's footprint in its bottom-right corner) and a one-sided `border-radius`
       of `16px 999px 999px 16px`. A four-sided `padding` shorthand + the
       `border-radius: 0` above reset BOTH, which is exactly what a full-screen
       stage needs. ⚠ Do not convert this to `padding-top:` only — the stage would
       inherit a 67px dead band at the bottom and a pill-shaped left edge. */
    padding: max(calc(env(safe-area-inset-top, 0px) + 6px),
                 calc(15vh + 112px + 20px))
             14px
             calc(env(safe-area-inset-bottom, 0px) + 12px);
    animation: eeh-orb-stage-in 0.26s cubic-bezier(0.22, 1, 0.36, 1);
  }

  /* ── the header strip: grip (left) — no close control (S-ORB-NO-MINUS) ──
   *
   * ⚠ THE `−` THAT USED TO SIT AT THE RIGHT OF THIS STRIP IS DELETED
   * (2026-10-10). Its `.eeh-orb-min` 44px disc and `:hover` rule lived here.
   * Shutting her off is now the ORB's own tap (`_bind()` toggles `_collapse()`),
   * which is a LARGER target than the 44px it replaces — the orb is 56px.
   *
   * ⚠ WHY THE STRIP ITSELF SURVIVES: it still carries the GRIP, and the grip is
   * what tells a phone visitor "this sheet can be pulled away". Removing the
   * close control must not remove the affordance that says the sheet is
   * dismissible, so the bar stays and only the button goes.
   *
   * ⚠ `justify-content` IS LEFT AT `space-between`: with one child it resolves
   * to flex-start, i.e. the grip sits left exactly as before. Changing it would
   * be churn with no visual effect. */
  .eeh-orb-mobile-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex: 0 0 auto;
    min-height: 44px;
    /* Absolute so it sits ABOVE the hero orb rather than pushing it down. */
    position: absolute;
    top: max(env(safe-area-inset-top, 0px), 4px);
    left: 0;
    right: 0;
    padding: 0 10px;
  }

  .eeh-orb-grip {
    /* A 40×4 pill: the universal "this can be dismissed / pulled away". */
    width: 40px;
    height: 4px;
    margin-left: 4px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.32);
  }

  /* ── her text, at phone-reading size ── */
  .eeh-orb-state { font-size: 13px; padding: 0 4px; }
  .eeh-orb-caption {
    /* The stage can afford far more than four lines — the cap is now only about
       never hiding the input, and 40vh cannot at any text size. */
    --eeh-orb-caption-max: 40vh;
    font-size: 17px;
    line-height: 1.55;
    padding: 0 4px;
  }

  /* Her text is CENTRED in the space between the hero and the input, so the stage
     reads as one composition instead of text clinging to the top. */
  .eeh-orb-head {
    flex: 1 1 auto;
    justify-content: center;
    gap: 10px;
    overflow: hidden;
  }

  /* ★ THE INPUT OWNS THE BOTTOM. `margin-top: auto` is explicit here so the input
     stays pinned no matter how long her reply grows — it must never move while
     she speaks. */
  .eeh-orb-form { flex: 0 0 auto; margin-top: auto; }
  .eeh-orb-options { flex: 0 0 auto; }

  /* ── thumb-sized controls, and no iOS focus zoom ── */
  /* ⚠ ID-SCOPED (S-ORB-INSTRUMENT-CAPSULE). A bare `.eeh-orb-input` here was
     (0,1,0) and would have lost this mobile padding/font-size to the same Neve
     rule that stole the desktop field. The scoped form must out-rank BOTH the
     theme AND the base scoped rule above, hence the extra `.eeh-orb-form`. */
  #eeh-orb-panel .eeh-orb-form .eeh-orb-input,
  #eeh-orb-panel .eeh-orb-form .eeh-orb-input:read-write {
    /* ⚠ 10px -> 11px VERTICAL (2026-10-11, measured by the mobile probe).
     *
     * The probe caught the field at 39.9px against a 40px tap-target floor — a
     * 0.1px miss, but a real one: the maths is
     *
     *     16px font x 1.12 line-height = 17.92px
     *     + 10px + 10px padding       = 20px
     *     + 2 x 1px border            =  2px
     *                                   ---------
     *                                   39.92px
     *
     * So `padding: 10px 14px` could NEVER reach 40px at this font size, and the
     * horizontal value was never the problem. 11px gives 41.9px with room to
     * spare, which matters because the field is the ONLY type-in target on a
     * screen with no hover state to re-point at it.
     *
     * ⚠ THIS WAS FOUND BY RUNNING THE PROBE, NOT BY READING THE CSS. The desktop
     *   rule and this one differ only in the vertical padding, and eyeballing the
     *   diff would not have surfaced a 0.1px shortfall. That is the whole reason
     *   `.dev/probe-orb-field-mobile.py` exists. */
    padding: 11px 14px;
    /* ★ 16px is the iOS threshold: at 15px or below, focusing the field zooms the
       whole page in and the visitor has to pinch back out.
       ⚠ THIS IS THE ONE NUMBER THAT MUST NOT BE SHRUNK FOR AESTHETICS. */
    font-size: 16px;
    border-radius: 999px;
  }
  /* ══ ★ THE CAPSULE ON THE PHONE STAGE (2026-10-11, S-ORB-INSTRUMENT-CAPSULE) ══
   *
   * ── WHAT THE PHONE KEEPS, AND WHY IT IS NOT THE DESKTOP TREATMENT ──────────
   * The stage field ALREADY kept a transparent fill and a 999px radius before this
   * pass — on mobile the field was never the white box the desktop reported,
   * because the stage's own full-screen surface meant an unreadable theme fill was
   * obvious and got fixed here first. So the look the desktop just adopted is the
   * look the phone has been running, and these rules keep the two SURFACES IN STEP
   * (§3 of the plugin rules: a component fixed in one must render in the other).
   *
   * ── ★★ 1.5px WAS TRIED AND MEASURED AWAY. THE HAIRLINE STAYS 1px. ──────────
   * The first version of this block raised the hairline to 1.5px, on the theory
   * that 1px of gold is too faint on a 390px screen in daylight. THE MEASUREMENT
   * FALSIFIED IT — and this is the third time in this pass that running the probe
   * beat reasoning about the values:
   *
   *   • `getComputedStyle().borderTopWidth` reported **1px at DPR 1, 2 AND 3**,
   *     even though the served sheet demonstrably contained `border-width: 1.5px`
   *     in a matching `(max-width: 480px)` block, AFTER the `border:` shorthand,
   *     at identical specificity. Chrome SNAPS sub-pixel border widths to whole
   *     CSS pixels at layout — the 1.5px was never delivered as 1.5px.
   *   • So the rule was a NO-OP IN EFFECT: it looked like a deliberate mobile
   *     refinement in the diff and produced exactly the same 1px hairline as not
   *     writing it. That is the read-but-never-applied shape this whole pass
   *     exists to eliminate, caught this time BEFORE shipping rather than after.
   *
   * ★ THE VISIBILITY PROBLEM IS THEREFORE SOLVED WITH A PROPERTY THAT RENDERS,
   *   NOT WITH A SUB-PIXEL WIDTH: the DPR-stable way to make a hairline read
   *   heavier on a dense screen is its COLOUR, not its width. On the phone the
   *   gold is taken to FULL opacity (desktop keeps the palette's own value), so
   *   the line is brighter on exactly the surface that needs it while the border
   *   geometry stays a crisp, predictable 1px on every device.
   *
   * ⚠ THIS IS THE ONE PLACE THE PHONE AND THE DESKTOP DELIBERATELY DIFFER, and
   *   the reason is deployment reality rather than taste: the phone is the surface
   *   where the field is the ONLY type-in affordance, with no hover to rediscover
   *   it. It is not a divergence to be "tidied up" later.
   *
   * ⚠ THE FILL STAYS FULLY TRANSPARENT. A phone surface has less room for doubt,
   *   so there is a temptation to add a faint wash — do not. The hull's own metal
   *   plus the hairline is legible, and a wash would reintroduce the exact class of
   *   bug this pass removed: a fill the theme can argue with. */
  #eeh-orb-panel .eeh-orb-form .eeh-orb-input,
  #eeh-orb-panel .eeh-orb-form .eeh-orb-input:read-write {
    border-color: var(--eeh-orb-gold, #CD853F);
    background: transparent;
  }

  .eeh-orb-row  { gap: 4px; }
  .eeh-orb-mute { width: 40px; height: 40px; font-size: 16px; }
  /* ⚠ NO `.eeh-orb-send` HERE (v7) — the button is deleted. The fold control on the
     stage is sized by the ≤480px block below, not by this one. */

  /* On a phone there is no hover, so the mute's relevance comes only from the
     engaged/state rules — but it is drawn brighter and larger because it competes
     with a thumb rather than a cursor. */
  .eeh-orb-panel .eeh-orb-mute { color: rgba(255, 255, 255, 0.85); }
}

@keyframes eeh-orb-stage-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ══════════════════════════════════════════════════════════════════════════
 * ★ SHORT VIEWPORT: THE KEYBOARD IS OPEN (2026-09-20, found by screenshot)
 *
 * THE BUG THE SCREENSHOT SHOWED: on a phone the soft keyboard takes roughly half
 * the viewport. At 390×420 the 112px hero orb still sat at `15vh`, which pushed
 * her body onto the TOP LINE of her own reply — she covered the words she was
 * saying, and the panel's `padding-top` (derived from the FULL-height orb) left
 * the text overlapping her portrait.
 *
 * ── THE RULE ──────────────────────────────────────────────────────────────
 * When vertical space is scarce, HER TEXT AND THE INPUT OUTRANK HER FACE. The
 * orb does not disappear — it shrinks back to a clear, tappable presence at the
 * top — because an orb that vanishes when you start typing is a worse loss than
 * one that gets smaller. A conversation is words.
 *
 * `max-height: 520px` is the trigger rather than a JS keyboard detection on
 * purpose: it needs no `visualViewport` listener, cannot get out of sync, and
 * covers every cause of a short viewport (keyboard, landscape, split screen,
 * a small device) with one rule.
 * ══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 480px) and (max-height: 520px) {
  #eeh-avp-orb.eeh-orb-staged {
    width: 64px;
    height: 64px;
    top: max(calc(env(safe-area-inset-top, 0px) + 8px), 8px);
  }
  #eeh-avp-orb.eeh-orb-staged .eeh-avp-icon { font-size: 26px; }
  #eeh-avp-orb.eeh-orb-staged .eeh-avp-ring { inset: -6px; border-width: 2px; }

  .eeh-orb-panel {
    /* Clear the shrunken orb (64px) + its 8px top offset + a real gap, so the
       orb's bottom edge can never reach into the text block. `+ 82px` is
       measured, not guessed: the probe reported `orbBottom=72, headTop=64` with
       the first attempt at `+ 64px`, i.e. the orb still ate 8px of the head. */
    padding-top: calc(env(safe-area-inset-top, 0px) + 82px);
    gap: 6px;
  }
  /* The header strip must not collide with the smaller orb: on a short viewport
     the − moves to the RIGHT EDGE at the same height as the orb's centre, so it
     still reads as "close" without stealing a row of vertical space.
     ⚠ S-ORB-NO-MINUS (2026-10-10): the − IS GONE, so this comment now describes
     only the GRIP. The `.eeh-orb-min { width:40px; height:40px }` sizing that
     used to sit here is deleted with the control. */
  .eeh-orb-mobile-bar {
    top: max(env(safe-area-inset-top, 0px), 2px);
    align-items: flex-start;
  }

  /* Her words lead. The status line steps back rather than pushing them down. */
  .eeh-orb-caption {
    --eeh-orb-caption-max: 44vh;
    font-size: 16px;
    line-height: 1.45;
  }
  .eeh-orb-state { font-size: 12px; }
  /* A slimmer field so the input always fits above the keyboard.
     ⚠ ID-SCOPED, same reason as the ≤480px block above. */
  #eeh-orb-panel .eeh-orb-form .eeh-orb-input,
  #eeh-orb-panel .eeh-orb-form .eeh-orb-input:read-write { padding: 8px 13px; }
  .eeh-orb-mute { width: 36px; height: 36px; }
}

@media (prefers-reduced-motion: reduce) {
  /* ★ `.eeh-orb-peek` IS INCLUDED (v7) — the pill is new, and every animated
     surface in this file must be named here or it silently ignores the preference. */
  .eeh-orb-panel, .eeh-orb-caption, .eeh-orb-input, .eeh-orb-peek { transition: none !important; }
}

/* ── ★ SMALL PHONES: the geometry contract's values, overridden once ─────
 * The orb is 52px at a 14px inset on a small screen (both still meet the project's
 * 44px touch minimum), and because `--eeh-orb-centre` is DERIVED from those two
 * numbers, the panel's alignment follows automatically:
 *
 *     --eeh-orb-centre = 14 + (52 / 2) = 40px
 *
 * ⚠ This is the whole reason the constants are custom properties on `:root`. The
 * old stylesheet hard-coded `right: 18px` on the panel and `right: 14px` on the
 * orb in a media query — two literals that had to be kept in agreement by hand,
 * which is exactly how a 28px misalignment survives a redesign.
 *
 * NOTE: this block sits ABOVE the four-state section deliberately — it is layout
 * only, and the states below own nothing it touches.
 */
@media (max-width: 480px) {
  :root {
    --eeh-orb-size: 71.8px;
    /* ★ S-ORB-PHONE-CLEAR (2026-10-12) — `12px` -> `24px`, SO THE GROUP LEAVES
     *   THE FLOOR SOME ROOM.
     *
     * ── THE OWNER'S REPORT ────────────────────────────────────────────────────
     *   "at mobile they are too low, hitting the bottom, can you leave some room
     *    there from the bottom."
     *
     * ⚠ THE PHONE WAS SITTING ON THE FLOOR WHILE THE DESKTOP WAS NOT. Desktop
     *   `--eeh-orb-inset` is `22px`, but THIS block dropped it to `12px` for the
     *   phone, and the "Hear this page" FAB's phone baseline was pinned at `12px`
     *   independently (`class-guide.php`). At 12px the orb — 71.8px tall, anchored
     *   to this inset — rested with its bottom edge barely clear of the viewport
     *   edge, which the eye reads as "hitting the bottom".
     *
     * ⚠ 24px IS THE PHONE'S OWN VALUE, NOT A COPY OF THE DESKTOP 22px. The phone
     *   is the most cramped surface and the thumb zone sits over that edge, so the
     *   owner's ask is satisfied by a touch MORE clearance than desktop, not less.
     *   It is a 12px lift from where the group sat.
     *
     * ⚠ THIS TOKEN MOVES THE ORB **AND** THE CAPTION HULL (`.eeh-orb-panel`), which
     *   is the point: the two are one group (she is the hull's right end cap), so
     *   moving one anchor moves both. The FAB's phone baseline (`--eeh-aov-baseline-phone`)
     *   is moved by the SAME amount in `class-guide.php` so the three stay on one
     *   line — that pairing is the whole reason this project publishes baseline
     *   tokens rather than keeping two literals in agreement by hand.
     *
     * ⚠ IF THIS EVER NEEDS TO CHANGE AGAIN, MOVE ALL THREE TOGETHER:
     *   • this token (`eeh-orb.css`)
     *   • `--eeh-aov-baseline-phone` (`class-guide.php`, the FAB)
     *   • its literal fallback (`eeh-audio-tour.css`)
     *   They are the single source of truth for the phone baseline, and a partial
     *   move is exactly how the two ends of the group drift apart on a phone. */
    --eeh-orb-inset: 24px;
  }
  /* ⚠ The orb's rules in the stage block below must not be undone by this one:
     `.eeh-orb-staged` sets its own `right`/`top` at the same specificity and later
     in the file, so the staged hero wins. Verified by geometry probe in
     `qa/orb_visual_test.py` (`V-SIZE-M`). */
}

/* ── the FOUR states ─────────────────────────────────────────────────── */

/* ★ MOTION LANGUAGE (2026-09-15)
 *
 * Each state gets a DISTINCT MOTION, not just a distinct colour. Colour alone
 * fails for colour-blind users and is hard to read at 56px; motion is legible
 * from across a room and needs no legend:
 *
 *   LISTENING → BREATHES  (slow in/out)      "I'm quiet, waiting for you"
 *   THINKING  → TRAVELS   (an arc rotates)   "working on it"
 *   SPEAKING  → RIPPLES   (pulses outward)   "sound is coming out"
 *
 * Text in the caption states the same thing in words, so the two channels
 * reinforce each other instead of competing.
 */

/* LISTENING — the ring breathes: calm, steady, inviting. */
#eeh-avp-orb.eeh-orb-listening .eeh-avp-ring {
  border-color: #4ade80;
  animation: eeh-orb-breathe 2.8s ease-in-out infinite;
}
/* ★ HER PORTRAIT IS NO LONGER HAZED WHILE SHE WAITS (2026-09-22, v4).
 *
 * The visitor: "make the hostess image little clearer, as looks like shaded".
 *
 * MEASURED CAUSES, three of them, and this is the largest: the portrait was
 * dropped to `opacity: 0.55` in LISTENING — which is her RESTING state, i.e. the
 * one the visitor looks at most of the time. She was at 55% brightness by default
 * and only reached full brightness mid-sentence.
 *
 * WHY 0.55 WAS THERE, AND WHY IT IS SAFE TO RAISE. It was the "your turn to talk"
 * cue. That cue is NOT lost by raising it, because it was never carried by this
 * opacity alone — the ring already encodes every state in MOTION, which the
 * "MOTION LANGUAGE" block above documents as the primary channel *precisely
 * because* colour and brightness fail (colour-blind users, a 56px target read from
 * across a room):
 *
 *     LISTENING → BREATHES (green, slow in/out)
 *     THINKING  → TRAVELS  (yellow arc)
 *     SPEAKING  → RIPPLES  (gold, outward)
 *
 * So the dimming was a SECOND, weaker channel for a signal that already has a
 * stronger one. Keeping a small step (0.85, not 1.0) preserves a hint of the
 * contrast between "waiting" and "talking" without the murk the visitor reported.
 *
 * ⚠ DO NOT DELETE THE RULE ENTIRELY. A flat 1.0 in every state removes the last
 * brightness difference between LISTENING and SPEAKING, and the `.clinerules`
 * record for this component treats the state legibility as load-bearing. 0.85 is
 * the deliberate middle: clear enough to read her face, distinct enough to matter. */
#eeh-avp-orb.eeh-orb-listening .eeh-avp-icon {
  opacity: 0.85;                     /* clear, with a hint of "your turn to talk" */
}

/* THINKING — a travelling arc: unmistakably "busy", never mistaken for calm. */
#eeh-avp-orb.eeh-orb-thinking .eeh-avp-ring {
  border-color: #facc15;
  border-top-color: transparent;
  border-right-color: transparent;
  animation: eeh-orb-travel 0.9s linear infinite;
}
#eeh-avp-orb.eeh-orb-thinking .eeh-avp-icon {
  opacity: 0.85;                     /* was 0.7 — see the LISTENING note above */
}

/* SPEAKING — the ring ripples outward: sound leaving her. */
#eeh-avp-orb.eeh-orb-speaking .eeh-avp-ring {
  border-color: #cd853f;
  animation: eeh-orb-ripple 1.6s ease-out infinite;
}
#eeh-avp-orb.eeh-orb-speaking .eeh-avp-icon {
  opacity: 1;                        /* full brightness: "I'm talking" */
}

/* GREETING — her intro. Visually identical to SPEAKING (she IS speaking) but
   kept as its own hook so onboarding can be styled without touching state. */
#eeh-avp-orb.eeh-orb-greeting .eeh-avp-ring {
  border-color: #cd853f;
  animation: eeh-orb-ripple 1.6s ease-out infinite;
}
#eeh-avp-orb.eeh-orb-greeting .eeh-avp-icon { opacity: 1; }

/* IDLE (no state class) — still ring, full-brightness portrait. */
#eeh-avp-orb .eeh-avp-icon { opacity: 1; }

/* The portrait itself gets a soft transition so the brightness change reads as
   "she responds" rather than a flicker. */
#eeh-avp-orb .eeh-avp-icon {
  transition: opacity 0.35s ease;
}

@keyframes eeh-orb-breathe {
  0%   { transform: scale(1);    opacity: 0.85; }
  50%  { transform: scale(1.09); opacity: 1;    }
  100% { transform: scale(1);    opacity: 0.85; }
}

@keyframes eeh-orb-travel {
  to { transform: rotate(360deg); }
}

@keyframes eeh-orb-ripple {
  0%   { transform: scale(1);    opacity: 0.9; }
  70%  { transform: scale(1.13); opacity: 0.25; }
  100% { transform: scale(1.18); opacity: 0;    }
}

/* ── accessibility: motion off means no motion ───────────────────────── */

@media (prefers-reduced-motion: reduce) {
  #eeh-avp-orb,
  #eeh-avp-orb .eeh-avp-ring,
  #eeh-avp-orb .eeh-avp-icon,
  #eeh-avp-orb .eeh-avp-label,
  .eeh-orb-panel,
  .eeh-orb-peek,
  .eeh-orb-head,
  .eeh-orb-caption,
  .eeh-orb-state,
  .eeh-orb-input,
  .eeh-orb-mute {
    animation: none !important;
    transition: none !important;
  }
  /* Without motion, colour + portrait brightness still tell the story. */
  #eeh-avp-orb.eeh-orb-listening .eeh-avp-ring { border-color: #4ade80; opacity: 1; }
  #eeh-avp-orb.eeh-orb-thinking  .eeh-avp-ring { border-color: #facc15; opacity: 1; }
  #eeh-avp-orb.eeh-orb-speaking  .eeh-avp-ring { border-color: #cd853f; opacity: 1; }
  #eeh-avp-orb.eeh-orb-greeting  .eeh-avp-ring { border-color: #cd853f; opacity: 1; }
}

/* ── small screens: keep the orb clear of the thumb zone ─────────────── */

@media (max-width: 480px) {
  #eeh-avp-orb {
    /*
     * ★ S-ORB-TRIO (2026-10-11) — THE TOKENS, NOT TWO MORE LITERALS.
     *
     * ⚠ THIS BLOCK USED TO HARDCODE `width/height: 52px` AND `right/bottom: 14px`
     *   — i.e. it fought the `:root` tokens the file's own header calls "the whole
     *   reason the constants are custom properties". The consequence was immediate
     *   and measurable: raising `--eeh-orb-size` to 59.8px for the 15%-bigger orb
     *   changed NOTHING on a phone, because this rule simply overrode it. The probe
     *   reported `h=52` while the token said 59.8.
     *
     * `--eeh-orb-size` is the single source of truth for her diameter and
     * `--eeh-orb-inset` for her edge offset, so the ≤480px block already sets both
     * on `:root` (see the geometry-constants block above). Referencing them here
     * means the phone follows any future resize automatically instead of silently
     * ignoring it.
     *
     * ⚠ `max(…, env(safe-area-inset-*))` IS KEPT, and it is not the same thing as
     *   the token: the token is the DESIGN inset, the env() is the DEVICE's demand
     *   for a notch/home-indicator. The larger of the two wins, which is what keeps
     *   her clear of hardware on a modern phone. The old block used a bare 14px and
     *   simply ignored the safe area.
     */
    width: var(--eeh-orb-size);
    height: var(--eeh-orb-size);   /* 59.8px — still far above the 44px touch minimum */
    right: max(var(--eeh-orb-inset), env(safe-area-inset-right, 0px));
    /* ══ ★★ S-ORB-PHONE-CLEAR (2026-10-12) — THE PHONE'S PILL-ALIGNMENT TERM IS
     *    GONE, BECAUSE THE PILL IT ALIGNED TO IS `display: none` HERE. ═══════════
     *
     * ── THE OWNER'S REPORT ────────────────────────────────────────────────────
     *   "at mobile they are too low, hitting the bottom, can you leave some room
     *    there from the bottom."
     *
     * ── ★ MEASURED, NOT GUESSED. THE TERM WAS THE CAUSE. ──────────────────────
     * The rule used to read (S-ORB-TRIO-2):
     *
     *     bottom: calc(max(inset, env(safe-area-inset-bottom)) - (orbSize - pillH) / 2)
     *
     * MEASURED at 390x844 with the inset at `24px`, her diameter at `71.8px` and the
     * phone pill height resolving to `38.4px`, that renders:
     *
     *     bottom = 24 - (71.8 - 38.4) / 2 = 24 - 16.7 = 7.3px   <- orb 7px off the floor
     *     fab    = 24px                                          <- the FAB cleared it by 24
     *
     * So the ORB — the thing the owner reported as "hitting the bottom" — was 16.7px
     * LOWER than the FAB it is supposed to sit beside. The FAB was never wrong; the
     * orb's SUBTRACTION was.
     *
     * ── WHY THE TERM WAS RIGHT ON DESKTOP AND WRONG HERE ──────────────────────
     * `(orbSize - pillH) / 2` exists to LIFT the shorter control's centre to meet the
     * orb's centre when the two sit side by side. But `.eeh-orb-peek` is
     * `display: none` at `max-width: 560px` (see that rule), so on a phone THERE IS NO
     * PILL TO ALIGN WITH. The subtraction therefore aligns her to a box that is not
     * rendered: it moves her DOWN by half the height difference and buys nothing. On
     * desktop the pill IS drawn, so the desktop rule keeps the term (S-ORB-TRIO-2).
     *
     * ⚠ SO THE TWO BREAKPOINTS NOW EXPRESS THEIR OWN TRUTH: desktop aligns to a
     *   visible pill; the phone rests on the baseline. That is not a divergence — it
     *   is the same "no element aligns to something it cannot see" principle the peek
     *   rule already states for the `pointer-events: none` case.
     *
     * ⚠ HER CENTRE IS STILL CONSISTENT WITH THE FAB: with no subtraction her bottom
     *   edge is `inset` (24px) and the FAB's bottom edge is `--eeh-aov-baseline-phone`
     *   (24px), so both rest on the same 24px line. The orb is taller, so its CENTRE
     *   sits ~16px above the FAB's — which is the same "taller box, higher centre"
     *   relationship the owner already accepted on desktop, not a new misalignment. */
    bottom: max(var(--eeh-orb-inset), env(safe-area-inset-bottom, 0px));
  }
  #eeh-avp-orb .eeh-avp-label {
    font-size: 12px;
    max-width: 62vw;
    white-space: normal;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * ★ QUESTIONS WITH OPTIONS (2026-09-19)
 *
 * When she does not know what the visitor wants she ASKS — and offers two or
 * three tap-able answers, so the visitor never has to compose a sentence to say
 * "yes" or "show me the famous people".
 *
 * ── DESIGN CONSTRAINTS (deliberate, do not "improve" these away) ──────────
 *   • The row ships `hidden` and is only revealed when options exist, so the
 *     panel is unchanged until she actually asks (OB-16: no dead UI).
 *   • Chips are quiet outlines, not gold blocks — the ORB is the interactive
 *     hero of this panel, and OB-17 keeps the send button quiet for the same
 *     reason. A row of filled buttons would read as a menu, which is exactly
 *     what she must NOT feel like.
 *   • Touch targets are >= 40px tall even though the text is small, because
 *     these are the primary way a phone visitor answers her question.
 *   • Wraps rather than scrolls: at most three short labels, so a wrap is
 *     always tidier than a horizontal scroller that hides the third option.
 * ══════════════════════════════════════════════════════════════════════════ */

.eeh-orb-options {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  margin: 0 0 9px;
  /* The panel is `position: fixed` over page content; keep the chips in the
   * panel's own stacking context so they can never float over the orb. */
  position: relative;
  z-index: 1;
}

.eeh-orb-options[hidden] { display: none; }

.eeh-orb-option {
  flex: 0 1 auto;
  max-width: 100%;
  min-height: 40px;
  padding: 9px 13px;
  border: 1px solid rgba(205, 133, 63, 0.55);   /* same gold as the send arrow */
  border-radius: 999px;                          /* pill = "an answer", not a tab */
  background: transparent;
  color: #f4f4f5;
  font: inherit;
  font-size: 13.5px;
  line-height: 1.25;
  text-align: left;
  cursor: pointer;
  touch-action: manipulation;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.eeh-orb-option:hover,
.eeh-orb-option:focus-visible {
  background: rgba(205, 133, 63, 0.18);
  border-color: #e09a52;
  outline: none;
}

/* A pressed chip gives immediate feedback even before the turn returns, so the
 * tap never feels ignored (the "quick thinking" half of the brief). */
.eeh-orb-option:active {
  background: rgba(205, 133, 63, 0.28);
}

@media (max-width: 480px) {
  .eeh-orb-options { gap: 6px; margin-bottom: 8px; }
  .eeh-orb-option  { font-size: 13px; padding: 9px 12px; }
}


/*
 * ★ S-EEH-NO-ROBOT (2026-10-14) — the "voice unavailable" notice.
 *
 * Shown by `_onVoiceUnavailable()` in eeh-orb-loop.js when the engine chain
 * (Edge -> Kokoro) could not produce audio. It exists so a silent orb is
 * EXPLAINED rather than merely broken, and it is the visible half of the rule
 * "never a machine voice": silence plus a message, never a robot reading her
 * lines. Deliberately quiet — a status chip, not an alert — and it auto-hides.
 */
.eeh-orb-voice-notice {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 2147483000;
  max-width: min(460px, calc(100vw - 32px));
  padding: 10px 16px;
  font-size: 13px;
  line-height: 1.4;
  text-align: center;
  color: #f6e3cd;
  background: rgba(26, 20, 14, 0.94);
  border: 1px solid rgba(205, 133, 63, 0.45);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.42);
  pointer-events: none;
}

@media (max-width: 480px) {
  .eeh-orb-voice-notice {
    bottom: 14px;
    font-size: 12px;
    padding: 9px 13px;
  }
}

